diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index fdcea617..a2fbeffb 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,7 +1,7 @@ { "$data": { "v6.7.0": { - "released_date": "2026/02/04", + "released_date": "2026/02/08", "feature": [ "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)", "版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)", @@ -96,6 +96,9 @@ "发行历史页面部分系统因字体差别导致统计数据显示不完整的问题", "部分设备无法正常初始化 MLKit Google OCR 的问题 (试修) _[`issue #8`](http://issues.autojs6.com/8#issuecomment-3117061768)_", "部分设备无法正常触发文件管理器功能按钮点击事件的问题 (试修) _[`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)_", "Canvas 构造函数可接受的参数类型错误 _[`issue #402`](http://issues.autojs6.com/402)_", "崩溃报告页面复制详细信息功能失效的问题", diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt index 98dfc69d..440fa2b9 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt @@ -28,8 +28,6 @@ import org.autojs.autojs.app.OnActivityResultDelegate.DelegateHost import org.autojs.autojs.core.permission.OnRequestPermissionsResultCallback import org.autojs.autojs.core.permission.PermissionRequestProxyActivity 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.storage.file.TmpScriptFiles 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.ViewUtils.onceGlobalLayout 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.databinding.ActivityEditBinding import java.io.File @@ -51,6 +51,8 @@ import java.io.IOException */ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity { + private var mReadOnly: Boolean = false + private val mOnBackPressedCallback = object : OnBackPressedCallback(true) { // override fun onBackPressed() { @@ -85,12 +87,17 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc super.onCreate(savedInstanceState) + mReadOnly = intent.getBooleanExtra(EditorView.EXTRA_READ_ONLY, false) + val binding = ActivityEditBinding.inflate(layoutInflater).also { setContentView(it.root) } mToolbar = findViewById(R.id.toolbar).apply { setTitleTextAppearance(this@EditActivity, R.style.TextAppearanceEditorTitle) setOnTitleViewClickListener { - EditableFileInfoDialogManager.showEditableFileInfoDialog(this@EditActivity, mEditorView.uri.path?.let { File(it) }) { - mEditorView.editor.text + val path = mEditorView.uri?.path + 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()) .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 setUpToolbar() @@ -130,6 +137,11 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc return true } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + mEditorMenu.prepareOptionsMenu(menu) + return super.onPrepareOptionsMenu(menu) + } + private fun TextView.adjustTitleTextView() = this.post { ValueAnimator.ofFloat(this.textSize, calculatedTextSize(this) ?: return@post).let { animator -> animator.duration = 120L @@ -247,7 +259,9 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc val menu = mode.menu 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() } super.onActionModeStarted(mode) diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java b/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java index 4a067287..3c2b3d91 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java @@ -2,15 +2,17 @@ package org.autojs.autojs.ui.edit; import android.annotation.SuppressLint; import android.content.Context; +import android.net.Uri; import android.text.InputType; import android.text.TextUtils; +import android.view.Menu; import android.view.MenuItem; import androidx.annotation.Nullable; import com.afollestad.materialdialogs.MaterialDialog; 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.Pref; -import org.autojs.autojs.util.MaterialDialogUtils; import org.autojs.autojs.model.indices.AndroidClass; import org.autojs.autojs.model.indices.ClassSearchingItem; 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. */ -@SuppressWarnings("ResultOfMethodCallIgnored") +@SuppressWarnings({"ResultOfMethodCallIgnored", "unused"}) @SuppressLint("CheckResult") public class EditorMenu { private final EditorView mEditorView; private final Context mContext; private final CodeEditor mEditor; + private final boolean mReadOnly; public EditorMenu(EditorView editorView) { + this(editorView, false); + } + + public EditorMenu(EditorView editorView, boolean readOnly) { mEditorView = editorView; mContext = editorView.getContext(); 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) { @@ -75,7 +115,7 @@ public class EditorMenu { .content(R.string.hint_long_click_run_to_debug) .positiveText(R.string.dialog_button_dismiss) .positiveColorRes(R.color.dialog_button_default); - MaterialDialogUtils.widgetThemeColor(builder); + DialogUtils.widgetThemeColor(builder); builder.show(); return tryDoing(mEditorView::debug); } @@ -216,7 +256,22 @@ public class EditorMenu { } 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() { @@ -256,7 +311,7 @@ public class EditorMenu { .onPositive((dialog, which) -> dialog.dismiss()) .autoDismiss(false); - MaterialDialogUtils.choiceWidgetThemeColor(builder); + DialogUtils.choiceWidgetThemeColor(builder); // TODO by SuperMonster003 on Oct 17, 2022. // ! Implementation for "scale view". @@ -338,12 +393,26 @@ public class EditorMenu { builder.positiveColorRes(R.color.dialog_button_attraction); builder.negativeText(R.string.dialog_button_cancel); builder.negativeColorRes(R.color.dialog_button_default); - MaterialDialogUtils.widgetThemeColor(builder); + DialogUtils.widgetThemeColor(builder); builder.show(); } 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); } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt index 5d441532..e73d52cd 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt @@ -26,7 +26,7 @@ import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers 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.setEditorTextSize 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.write 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.HistoryUriUtils 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.DisplayUtils.pxToSp 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.StringUtils +import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils.showSnack import org.autojs.autojs.util.ViewUtils.showToast import org.autojs.autojs6.R +import org.autojs.autojs6.R.string.text_unknown import org.autojs.autojs6.databinding.EditorViewBinding import java.io.File import java.io.IOException @@ -104,9 +107,15 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag @JvmField 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 private set @@ -204,19 +213,31 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } fun handleIntent(intent: Intent): Observable { - 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) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { - mReadOnly = intent.getBooleanExtra(EXTRA_READ_ONLY, false) val saveEnabled = intent.getBooleanExtra(EXTRA_SAVE_ENABLED, true) - if (mReadOnly || !saveEnabled) { + if (!saveEnabled) { + findViewById(R.id.save).visibility = GONE + } else if (readOnly) { + findViewById(R.id.undo).visibility = GONE + findViewById(R.id.redo).visibility = GONE findViewById(R.id.save).visibility = GONE } if (!intent.getBooleanExtra(EXTRA_RUN_ENABLED, true)) { findViewById(R.id.run).visibility = GONE } - if (mReadOnly) { + if (readOnly) { editor.setReadOnly(true) } } @@ -228,21 +249,28 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } private fun handleText(intent: Intent): Observable { - val path = intent.getStringExtra(EXTRA_PATH) val content = intent.getStringExtra(EXTRA_CONTENT) if (content != null) { setInitialText(content) - return Observable.just(content) } - uri = if (path == null) { - intent.data ?: return Observable.error(IllegalArgumentException("path and content is empty")) - } else { - Uri.fromFile(File(path)) + + val path = intent.getStringExtra(EXTRA_PATH) + val uri = path?.let { + 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") @@ -295,7 +323,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag private fun initNormalToolbar() { mNormalToolbar.apply { 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() } @@ -375,6 +408,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag mCodeCompletionBar.setTextColor(textColor) mSymbolBar.setTextColor(textColor) mShowFunctionsButton.setColorFilter(textColor) + ViewUtils.setNavigationBarBackgroundColor(activity, it.imeBarBackgroundColor) invalidate() } } @@ -412,7 +446,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } @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 if (showMessage) { showSnack(this, R.string.text_start_running) @@ -420,7 +454,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // TODO by Stardust on Oct 24, 2018. val execution = runWithBroadcastSender( file, - workingDirectory = uri.path?.let { File(it).parent }, + workingDirectory = uri?.path?.let { File(it).parent }, overriddenFullPath, ) ?: return null scriptExecutionId = execution.id @@ -428,8 +462,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag return execution } - private fun runTmpFile(file: File? = uri.path?.let { File(it) }): ScriptExecution? { - return run(true, file, uri.path) + private fun runTmpFile(file: File? = uri?.path?.let { File(it) }): ScriptExecution? { + return run(true, file, uri?.path) } private fun undo() = editor.undo() @@ -439,10 +473,17 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag fun save(): Observable = Observable .fromCallable { - // Use a transactional save flow for both file:// and content://. - // zh-CN: 对 file:// 与 content:// 统一使用事务式保存流程. - editor.text.apply { - writeTextWithCharsetTransactional(uri, this) + when (val uri = uri) { + null -> { + throw IllegalStateException(context.getString(R.string.error_unable_to_save_file_as_current_file_path_is_unknown)) + } + else -> { + // Use a transactional save flow for both file:// and content://. + // zh-CN: 对 file:// 与 content:// 统一使用事务式保存流程. + editor.text.apply { + writeTextWithCharsetTransactional(uri, this) + } + } } } .subscribeOn(Schedulers.io()) @@ -506,7 +547,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag if (logicalPath != null && shouldTrackHistory(oldBytes, newBytes) && oldBytes != null) { runCatching { HistoryRepository(context.applicationContext).recordSavePre( - uri = uri, logicalPath = logicalPath, oldBytes = oldBytes, encodingName = targetCharset.name(), @@ -689,7 +729,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag private fun shouldTrackHistory(oldBytes: ByteArray?, newBytes: ByteArray): Boolean { val oldSize = oldBytes?.size ?: 0 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 { @@ -711,18 +753,32 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } fun showVersionHistoryDialog() { - // 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) - }, - ) + when (val uri = uri) { + null -> { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(context) + .title(R.string.text_prompt) + .content(R.string.error_unable_to_display_version_history_as_current_file_path_is_unknown) + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) + .build() + } + } + 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. @@ -757,9 +813,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag val now = System.currentTimeMillis() - // 1) Remove expired (older than 7 days). - // zh-CN: 1) 删除过期草稿 (超过 7 天). - val expiredBefore = now - DRAFT_MAX_DAYS_MS + // 1) Remove expired. + // zh-CN: 1) 删除过期草稿. + val maxDraftLifetime = HistoryPrefs.draftsMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L + val expiredBefore = now - maxDraftLifetime files.forEach { f -> if (f.lastModified() < expiredBefore) { // noinspection ResultOfMethodCallIgnored @@ -772,10 +829,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag val remained = draftsDir.listFiles()?.toList()?.sortedByDescending { it.lastModified() } ?: return 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()) { - if (total <= DRAFT_MAX_TOTAL_BYTES) break + if (total <= draftLimit) break val len = f.length().coerceAtLeast(0L) if (f.delete()) { total -= len @@ -823,7 +882,16 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } 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() { @@ -843,10 +911,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } fun selectTextSize() { - TextSizeSettingDialogBuilder(context) - .initialValue(pxToSp(editor.codeEditText.textSize).toInt()) - .callback { value: Int -> setTextSize(value) } - .show() + DialogUtils.buildAndShowAdaptive { + TextSizeSettingDialogBuilder(context) + .initialValue(pxToSp(editor.codeEditText.textSize).toInt()) + .callback { value: Int -> setTextSize(value) } + .build() + } } fun setTextSize(value: Int) { @@ -860,22 +930,24 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag if (i < 0) { i = 0 } - MaterialDialog.Builder(context) - .title(R.string.text_editor_theme) - .items(themes) - .choiceWidgetThemeColor() - .itemsCallbackSingleChoice(i) { _, _, which, _ -> - themes[which]?.let { - setTheme(it) - Themes.setCurrent(it.name) + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(context) + .title(R.string.text_editor_theme) + .items(themes) + .choiceWidgetThemeColor() + .itemsCallbackSingleChoice(i) { _, _, which, _ -> + themes[which]?.let { + setTheme(it) + Themes.setCurrent(it.name) + } + true } - true - } - .negativeText(R.string.dialog_button_cancel) - .negativeColorRes(R.color.dialog_button_default) - .positiveText(R.string.dialog_button_confirm) - .positiveColorRes(R.color.dialog_button_attraction) - .show() + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_attraction) + .build() + } } @Throws(CheckedPatternSyntaxException::class) @@ -910,7 +982,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag .replace(R.id.toolbar_menu, DebugToolbarFragment()) .commit() debugBar.visibility = VISIBLE - mInputMethodEnhanceBar.visibility = GONE + if (!mReadOnly) { + mInputMethodEnhanceBar.visibility = GONE + } mDebugging = true } @@ -923,7 +997,9 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag showNormalToolbar() editor.setDebuggingLine(-1) debugBar.visibility = GONE - mInputMethodEnhanceBar.visibility = VISIBLE + if (!mReadOnly) { + mInputMethodEnhanceBar.visibility = VISIBLE + } mDebugging = false } @@ -1071,18 +1147,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag private const val MIN_CONFIDENCE_TO_WRITE_FILE = 90 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). // zh-CN: 内部存储根目录 (不访问外置 SD). 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_RUN_ENABLED = "runEnabled" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt index 8d7492d5..0f47faa6 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt @@ -33,7 +33,11 @@ import android.os.Parcelable import android.text.Layout import android.util.AttributeSet import android.view.Gravity +import android.view.KeyEvent import android.view.MotionEvent +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import android.view.inputmethod.InputConnectionWrapper import android.widget.TextViewHelper import androidx.appcompat.widget.AppCompatEditText import androidx.core.graphics.withTranslation @@ -68,6 +72,14 @@ class CodeEditText : AppCompatEditText { private var mDebuggingLine = -1 private var mCursorChangeCallbacks: CopyOnWriteArrayList? = 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 get() = layout?.let { LayoutHelper.getLineOfChar(it, selectionStart) } ?: -1 @@ -99,6 +111,86 @@ class CodeEditText : AppCompatEditText { importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO } 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") @@ -237,23 +329,32 @@ class CodeEditText : AppCompatEditText { // Draw code 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 (lineVisibleEnd > textLength) continue + if (lineEnd > textLength) continue - // @Reference to LYS86 (https://github.com/LYS86) by SuperMonster003 on Apr 17, 2025. - // ! https://github.com/LYS86/AutoJs/blob/05a7e48a8d5b0c6207b3d2974f762c050156298c/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java#L232 - if (lineStart == lineVisibleEnd) continue + // If this is an empty line (or the line only contains line-break chars), skip drawing. + // zh-CN: 如果这是空白行(或该行只包含换行字符), 则跳过绘制. + if (lineStart >= lineEnd) continue - val lineEnd = lineVisibleEnd.coerceAtMost(highlightTokens.colors.size) + val localColors = highlightTokens.colors val visibleCharStart = getVisibleCharIndex(paint, scrollX, lineStart, lineEnd) var visibleCharEnd = getVisibleCharIndex(paint, scrollX + mParentScrollView!!.width, lineStart, lineEnd) + 1 - val safeText = text ?: continue - val localColors = highlightTokens.colors - if (visibleCharStart >= visibleCharEnd) continue if (visibleCharStart >= safeText.length) continue if (visibleCharStart < 0) continue @@ -290,7 +391,7 @@ class CodeEditText : AppCompatEditText { } paint.color = previousColor - visibleCharEnd = visibleCharEnd.coerceAtMost(textLength) + visibleCharEnd = minOf(visibleCharEnd.coerceAtMost(textLength), lineEnd) if (previousColorPos >= visibleCharEnd) continue val currentText = text ?: continue diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt index ee2ccd8f..8877e58c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt @@ -51,8 +51,9 @@ import kotlin.math.floor * Modified by project: https://github.com/980008027/JsDroidEditor */ /** - * Modified by SuperMonster003 as of 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 { @@ -293,7 +294,7 @@ class CodeEditor : HVScrollView { } fun setReadOnly(readOnly: Boolean) { - codeEditText.isEnabled = !readOnly + codeEditText.setReadOnly(readOnly) } fun setRedoUndoEnabled(enabled: Boolean) { diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java index 0cf908ec..7f5d7450 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java @@ -1,5 +1,6 @@ package org.autojs.autojs.ui.edit.toolbar; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -93,7 +94,10 @@ public class DebugToolbarFragment extends ToolbarFragment(this)); setInterrupted(false); - mCurrentEditorSourceUrl = mInitialEditorSourceUrl = mEditorView.uri.toString(); + Uri uri = mEditorView.getUri(); + if (uri != null) { + mCurrentEditorSourceUrl = mInitialEditorSourceUrl = uri.toString(); + } mInitialEditorSource = mEditorView.editor.getText(); setupEditor(); ScriptExecution execution = mEditorView.run(false);