mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(editor): use coroutines for reading/writing files in CodeEditorView
This commit is contained in:
@@ -79,7 +79,7 @@ abstract class BaseBuildAction : EditorActivityAction() {
|
||||
return
|
||||
}
|
||||
|
||||
activity.saveAll()
|
||||
activity.saveAllAsync()
|
||||
|
||||
buildService.executeTasks(tasks = tasks).whenComplete { result, err ->
|
||||
if (result == null || err != null) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.itsaky.androidide.actions.BaseBuildAction
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.resources.R.string
|
||||
import com.itsaky.androidide.utils.flashSuccess
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Triggers a project sync request.
|
||||
@@ -33,7 +34,7 @@ import com.itsaky.androidide.utils.flashSuccess
|
||||
class ProjectSyncAction(context: Context, override val order: Int) : BaseBuildAction() {
|
||||
|
||||
override val id: String = "action_editor_syncProject"
|
||||
override var requiresUIThread = true
|
||||
override var requiresUIThread = false
|
||||
|
||||
init {
|
||||
label = context.getString(string.title_sync_project)
|
||||
@@ -41,11 +42,13 @@ class ProjectSyncAction(context: Context, override val order: Int) : BaseBuildAc
|
||||
}
|
||||
|
||||
override fun execAction(data: ActionData): Any {
|
||||
return data.getActivity()!!.apply {
|
||||
saveAllResult()
|
||||
flashSuccess(string.all_saved)
|
||||
|
||||
initializeProject()
|
||||
return data.requireActivity().apply {
|
||||
runBlocking { saveAll() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
val activity = data.requireActivity()
|
||||
activity.initializeProject()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.itsaky.androidide.activities.editor.EditorHandlerActivity
|
||||
import com.itsaky.androidide.editor.ui.IDEEditor
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.uidesigner.UIDesignerActivity
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@@ -38,6 +39,8 @@ class PreviewLayoutAction(context: Context, override val order: Int) : EditorRel
|
||||
|
||||
override val id: String = "editor_previewLayout"
|
||||
|
||||
override var requiresUIThread: Boolean = false
|
||||
|
||||
init {
|
||||
label = context.getString(R.string.title_preview_layout)
|
||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_preview_layout)
|
||||
@@ -87,13 +90,18 @@ class PreviewLayoutAction(context: Context, override val order: Int) : EditorRel
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Migrate to coroutines
|
||||
override fun execAction(data: ActionData): Boolean {
|
||||
val activity = data.getActivity()!!
|
||||
activity.saveAll()
|
||||
activity.previewLayout(data.requireEditor().file!!)
|
||||
val activity = data.requireActivity()
|
||||
runBlocking { activity.saveAll() }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
val activity = data.requireActivity()
|
||||
activity.previewLayout(data.requireEditor().file!!)
|
||||
}
|
||||
|
||||
private fun EditorHandlerActivity.previewLayout(file: File) {
|
||||
val intent = Intent(this, UIDesignerActivity::class.java)
|
||||
intent.putExtra(UIDesignerActivity.EXTRA_FILE, file.absolutePath)
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import com.itsaky.androidide.utils.flashSuccess
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/** @author Akash Yadav */
|
||||
class SaveFileAction(context: Context, override val order: Int) : EditorRelatedAction() {
|
||||
@@ -35,6 +36,8 @@ class SaveFileAction(context: Context, override val order: Int) : EditorRelatedA
|
||||
|
||||
override val id: String = "editor_saveFile"
|
||||
|
||||
override var requiresUIThread: Boolean = false
|
||||
|
||||
init {
|
||||
label = context.getString(R.string.save)
|
||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_save)
|
||||
@@ -60,7 +63,7 @@ class SaveFileAction(context: Context, override val order: Int) : EditorRelatedA
|
||||
return try {
|
||||
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
|
||||
// and saveAll call will result in UI actions
|
||||
ResultWrapper(context.saveAllResult())
|
||||
ResultWrapper(runBlocking { context.saveAllResult() })
|
||||
} catch (error: Throwable) {
|
||||
log.error("Failed to save file", error)
|
||||
ResultWrapper()
|
||||
|
||||
@@ -68,6 +68,7 @@ import com.itsaky.androidide.models.SearchResult
|
||||
import com.itsaky.androidide.preferences.internal.launchAppAfterInstall
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.projects.ProjectManagerImpl
|
||||
import com.itsaky.androidide.tasks.cancelIfActive
|
||||
import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout
|
||||
import com.itsaky.androidide.ui.editor.CodeEditorView
|
||||
import com.itsaky.androidide.uidesigner.UIDesignerActivity
|
||||
@@ -81,6 +82,8 @@ import com.itsaky.androidide.viewmodel.EditorViewModel
|
||||
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
|
||||
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
|
||||
import com.itsaky.androidide.xml.widgets.WidgetTableRegistry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode.MAIN
|
||||
import java.io.File
|
||||
@@ -104,6 +107,11 @@ abstract class BaseEditorActivity :
|
||||
|
||||
protected val log: ILogger = ILogger.newInstance("EditorActivity")
|
||||
|
||||
/**
|
||||
* [CoroutineScope] for executing tasks in the background.
|
||||
*/
|
||||
protected val activityBackroundScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
internal var installationCallback: ApkInstallationSessionCallback? = null
|
||||
|
||||
var uiDesignerResultLauncher: ActivityResultLauncher<Intent>? = null
|
||||
@@ -138,8 +146,6 @@ abstract class BaseEditorActivity :
|
||||
|
||||
protected abstract fun doOpenFile(file: File, selection: Range?)
|
||||
|
||||
protected abstract fun doSaveAll(): Boolean
|
||||
|
||||
protected abstract fun doDismissSearchProgress()
|
||||
|
||||
protected abstract fun getOpenedFiles(): List<OpenedFile>
|
||||
@@ -149,6 +155,10 @@ abstract class BaseEditorActivity :
|
||||
protected open fun preDestroy() {
|
||||
installationCallback?.destroy()
|
||||
installationCallback = null
|
||||
|
||||
if (isDestroying) {
|
||||
activityBackroundScope.cancelIfActive("Activity is being destroyed")
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun postDestroy() {
|
||||
@@ -389,11 +399,10 @@ abstract class BaseEditorActivity :
|
||||
return
|
||||
}
|
||||
val view = provideCurrentEditor()
|
||||
if (view?.editor == null) {
|
||||
val text = view?.editor?.text ?: run {
|
||||
log.warn("No file opened to append UI designer result")
|
||||
return
|
||||
}
|
||||
val text = view.editor.text
|
||||
val endLine = text.lineCount - 1
|
||||
text.replace(0, 0, endLine, text.getColumnCount(endLine), generated)
|
||||
}
|
||||
@@ -424,7 +433,7 @@ abstract class BaseEditorActivity :
|
||||
log.debug(
|
||||
"onBuildStatusChanged: isInitializing: ${editorViewModel.isInitializing}, isBuildInProgress: ${editorViewModel.isBuildInProgress}")
|
||||
val visible = editorViewModel.isBuildInProgress || editorViewModel.isInitializing
|
||||
binding.buildProgressIndicator.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
binding.progressIndicator.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
@@ -509,9 +518,7 @@ abstract class BaseEditorActivity :
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
val editor = provideCurrentEditor()
|
||||
if (editor?.editor != null) {
|
||||
editor.editor.ensureWindowsDismissed()
|
||||
}
|
||||
editor?.editor?.ensureWindowsDismissed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-34
@@ -55,6 +55,9 @@ import com.itsaky.androidide.utils.DialogUtils.newYesNoDialog
|
||||
import com.itsaky.androidide.utils.IntentUtils.openImage
|
||||
import com.itsaky.androidide.utils.UniqueNameBuilder
|
||||
import com.itsaky.androidide.utils.flashSuccess
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.File
|
||||
@@ -80,10 +83,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
closeAll(runAfter)
|
||||
}
|
||||
|
||||
override fun doSaveAll(): Boolean {
|
||||
return saveAll()
|
||||
}
|
||||
|
||||
override fun provideCurrentEditor(): CodeEditorView? {
|
||||
return getCurrentEditor()
|
||||
}
|
||||
@@ -102,7 +101,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
mBuildEventListener.setActivity(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
editorViewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it }
|
||||
editorViewModel._displayedFile.observe(
|
||||
this) { this.binding.editorContainer.displayedChild = it }
|
||||
editorViewModel._startDrawerOpened.observe(this) { opened ->
|
||||
this.binding.editorDrawerLayout.apply {
|
||||
if (opened) openDrawer(GravityCompat.START) else closeDrawer(GravityCompat.START)
|
||||
@@ -224,13 +224,12 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
|
||||
override fun openFileAndSelect(file: File, selection: Range?) {
|
||||
openFile(file, selection)
|
||||
val opened = getEditorForFile(file)
|
||||
if (opened?.editor != null) {
|
||||
val editor = opened.editor
|
||||
editor.post {
|
||||
|
||||
getEditorForFile(file)?.editor?.also { editor ->
|
||||
editor.postInLifecycle {
|
||||
if (selection == null) {
|
||||
editor.setSelection(0, 0)
|
||||
return@post
|
||||
return@postInLifecycle
|
||||
}
|
||||
|
||||
editor.validateRange(selection)
|
||||
@@ -318,8 +317,24 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun saveAll(notify: Boolean, processResources: Boolean): Boolean {
|
||||
val result = saveAllResult()
|
||||
override fun saveAllAsync(
|
||||
notify: Boolean,
|
||||
processResources: Boolean,
|
||||
progressConsumer: ((Int, Int) -> Unit)?,
|
||||
runAfter: (() -> Unit)?
|
||||
) {
|
||||
activityBackroundScope.launch {
|
||||
saveAll(notify, processResources, progressConsumer)
|
||||
runAfter?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAll(
|
||||
notify: Boolean,
|
||||
processResources: Boolean,
|
||||
progressConsumer: ((Int, Int) -> Unit)?
|
||||
): Boolean {
|
||||
val result = saveAllResult(progressConsumer)
|
||||
|
||||
if (notify) {
|
||||
flashSuccess(string.all_saved)
|
||||
@@ -336,21 +351,22 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
return result.gradleSaved
|
||||
}
|
||||
|
||||
override fun saveAllResult(): SaveResult {
|
||||
override suspend fun saveAllResult(progressConsumer: ((Int, Int) -> Unit)?): SaveResult {
|
||||
val result = SaveResult()
|
||||
for (i in 0 until editorViewModel.getOpenedFileCount()) {
|
||||
saveResult(i, result)
|
||||
progressConsumer?.invoke(i + 1, editorViewModel.getOpenedFileCount())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun saveResult(index: Int, result: SaveResult) {
|
||||
override suspend fun saveResult(index: Int, result: SaveResult) {
|
||||
if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
|
||||
return
|
||||
}
|
||||
|
||||
val frag = getEditorAtIndex(index)
|
||||
if (frag?.file == null) {
|
||||
val frag = getEditorAtIndex(index) ?: return
|
||||
if (frag.file == null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -379,11 +395,12 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
}
|
||||
|
||||
val finalModified = modified
|
||||
ThreadUtils.runOnUiThread {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
editorViewModel.setFilesModified(finalModified)
|
||||
|
||||
// set tab as unmodified
|
||||
val tab = binding.tabs.getTabAt(index) ?: return@runOnUiThread
|
||||
val tab = binding.tabs.getTabAt(index) ?: return@withContext
|
||||
if (tab.text!!.startsWith('*')) {
|
||||
tab.text = tab.text!!.substring(startIndex = 1)
|
||||
}
|
||||
@@ -409,12 +426,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
return
|
||||
}
|
||||
|
||||
if (editor?.editor != null) {
|
||||
editor.editor.apply {
|
||||
notifyClose()
|
||||
release()
|
||||
}
|
||||
} else {
|
||||
editor?.close() ?: run {
|
||||
log.error("Cannot save file before close. Editor instance is null")
|
||||
}
|
||||
|
||||
@@ -435,7 +447,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
|
||||
override fun closeOthers() {
|
||||
val unsavedFiles =
|
||||
editorViewModel.getOpenedFiles().map(::getEditorForFile).filter { it != null && it.isModified }
|
||||
editorViewModel.getOpenedFiles().map(::getEditorForFile)
|
||||
.filter { it != null && it.isModified }
|
||||
|
||||
if (unsavedFiles.isEmpty()) {
|
||||
val file = editorViewModel.getCurrentFile()
|
||||
@@ -474,7 +487,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
|
||||
if (currentEditor != null) {
|
||||
data.put(IDEEditor::class.java, currentEditor.editor)
|
||||
data.put(File::class.java, currentEditor.editor.file)
|
||||
data.put(File::class.java, currentEditor.file)
|
||||
}
|
||||
|
||||
getInstance().fillMenu(FillMenuParams(data, EDITOR_TOOLBAR, menu))
|
||||
@@ -484,7 +497,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
override fun closeAll(runAfter: () -> Unit) {
|
||||
val count = editorViewModel.getOpenedFileCount()
|
||||
val unsavedFiles =
|
||||
editorViewModel.getOpenedFiles().map(this::getEditorForFile).filter { it != null && it.isModified }
|
||||
editorViewModel.getOpenedFiles().map(this::getEditorForFile)
|
||||
.filter { it != null && it.isModified }
|
||||
|
||||
if (unsavedFiles.isNotEmpty()) {
|
||||
// There are unsaved files
|
||||
@@ -494,11 +508,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
|
||||
// Files were already saved, close all files one by one
|
||||
for (i in 0 until count) {
|
||||
val editor = getEditorAtIndex(i)
|
||||
if (editor?.editor != null) {
|
||||
editor.editor.notifyClose()
|
||||
editor.editor.release()
|
||||
} else {
|
||||
getEditorAtIndex(i)?.close() ?: run {
|
||||
log.error("Unable to close file at index:", i)
|
||||
}
|
||||
}
|
||||
@@ -538,8 +548,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
|
||||
message = getString(string.msg_files_unsaved, TextUtils.join("\n", mapped)),
|
||||
positiveClickListener = { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
saveAll(true)
|
||||
invokeAfter.run()
|
||||
saveAllAsync(notify = true, runAfter = { runOnUiThread(invokeAfter) })
|
||||
}
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
||||
@@ -39,12 +39,43 @@ interface IEditorHandler {
|
||||
fun openFileAndGetIndex(file: File, selection: Range?) : Int
|
||||
|
||||
fun areFilesModified(): Boolean
|
||||
|
||||
fun saveAll() : Boolean = saveAll(true)
|
||||
fun saveAll(notify: Boolean) : Boolean = saveAll(notify, false)
|
||||
fun saveAll(notify: Boolean, processResources: Boolean) : Boolean
|
||||
fun saveAllResult() : SaveResult
|
||||
fun saveResult(index: Int, result: SaveResult)
|
||||
|
||||
/**
|
||||
* Save all files.
|
||||
*
|
||||
* @param notify Whether to notify the user about the save event.
|
||||
* @param processResources Whether the resources must be generated after the save operation.
|
||||
* @param progressConsumer A function which consumes the progress of the save operation.
|
||||
* See [saveAllResult] for more details.
|
||||
*/
|
||||
suspend fun saveAll(
|
||||
notify: Boolean = true,
|
||||
processResources: Boolean = false,
|
||||
progressConsumer: ((progress: Int, total: Int) -> Unit)? = null
|
||||
) : Boolean
|
||||
|
||||
/**
|
||||
* Save all files asynchronously.
|
||||
*
|
||||
* @param runAfter A callback function which will be run after the files are saved.
|
||||
* @see saveAll
|
||||
*/
|
||||
fun saveAllAsync(
|
||||
notify: Boolean = true,
|
||||
processResources: Boolean = false,
|
||||
progressConsumer: ((progress: Int, total: Int) -> Unit)? = null,
|
||||
runAfter: (() -> Unit)? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Save all files and get the [SaveResult].
|
||||
*
|
||||
* @param progressConsumer A function which consumes the progress of the save operation. The first
|
||||
* parameter of the function is the current save progress (saved file count) and the second parameter
|
||||
* is the total file count.
|
||||
*/
|
||||
suspend fun saveAllResult(progressConsumer: ((progress: Int, total: Int) -> Unit)? = null) : SaveResult
|
||||
suspend fun saveResult(index: Int, result: SaveResult)
|
||||
|
||||
fun closeFile(index: Int) = closeFile(index) {}
|
||||
fun closeFile(index: Int, runAfter: () -> Unit)
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.ui.editor;
|
||||
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.DELETE_EMPTY_LINES;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.DELETE_TABS_ON_BACKSPACE;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_LINE_BREAK;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_PASSWORD;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_WS_EMPTY_LINE;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_WS_INNER;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_WS_LEADING;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_WS_TRAILING;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FONT_LIGATURES;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FONT_SIZE;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.STICKY_SCROLL_ENABLED;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_CUSTOM_FONT;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_ICU;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_MAGNIFER;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.WORD_WRAP;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDeleteEmptyLines;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDeleteTabsOnBackspace;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawEmptyLineWs;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawInnerWs;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawLeadingWs;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawLineBreak;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawTrailingWs;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getFontLigatures;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getFontSize;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getStickyScrollEnabled;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getUseIcu;
|
||||
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getUseMagnifier;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.blankj.utilcode.util.FileIOUtils;
|
||||
import com.blankj.utilcode.util.FileUtils;
|
||||
import com.blankj.utilcode.util.SizeUtils;
|
||||
import com.itsaky.androidide.app.BaseApplication;
|
||||
import com.itsaky.androidide.editor.databinding.LayoutCodeEditorBinding;
|
||||
import com.itsaky.androidide.editor.ui.EditorSearchLayout;
|
||||
import com.itsaky.androidide.editor.ui.IDEEditor;
|
||||
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent;
|
||||
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
|
||||
import com.itsaky.androidide.lsp.api.ILanguageServer;
|
||||
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry;
|
||||
import com.itsaky.androidide.lsp.java.JavaLanguageServer;
|
||||
import com.itsaky.androidide.lsp.xml.XMLLanguageServer;
|
||||
import com.itsaky.androidide.models.Range;
|
||||
import com.itsaky.androidide.preferences.internal.EditorPreferencesKt;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.FileUtil;
|
||||
import com.itsaky.androidide.utils.ILogger;
|
||||
import com.itsaky.androidide.utils.TypefaceUtilsKt;
|
||||
import io.github.rosemoe.sora.text.LineSeparator;
|
||||
import io.github.rosemoe.sora.widget.component.Magnifier;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A view that handles opened code editors.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@SuppressLint("ViewConstructor") // This view is always dynamically created.
|
||||
public class CodeEditorView extends LinearLayout {
|
||||
|
||||
private static final ILogger LOG = ILogger.newInstance("CodeEditorView");
|
||||
private final File file;
|
||||
private final LayoutCodeEditorBinding binding;
|
||||
private final EditorSearchLayout searchLayout;
|
||||
|
||||
public CodeEditorView(
|
||||
@NonNull Context context, @NonNull File file, final @NonNull Range selection) {
|
||||
super(context);
|
||||
this.file = file;
|
||||
|
||||
final var inflater = LayoutInflater.from(context);
|
||||
this.binding = LayoutCodeEditorBinding.inflate(inflater);
|
||||
this.binding.editor.setHighlightCurrentBlock(true);
|
||||
this.binding.editor.getProps().autoCompletionOnComposing = true;
|
||||
this.binding.editor.setDividerWidth(SizeUtils.dp2px(1));
|
||||
this.binding.editor.setColorScheme(SchemeAndroidIDE.newInstance(context));
|
||||
this.binding.editor.setLineSeparator(LineSeparator.LF);
|
||||
|
||||
this.searchLayout = new EditorSearchLayout(context, this.binding.editor);
|
||||
|
||||
this.binding.diagnosticTextContainer.setVisibility(GONE);
|
||||
|
||||
removeAllViews();
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
addView(this.binding.getRoot(), new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
addView(
|
||||
this.searchLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
|
||||
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
final var contents = FileIOUtils.readFile2String(file);
|
||||
final var editor = getEditor();
|
||||
editor.post(() -> {
|
||||
editor.setText(contents, createEditorArgs());
|
||||
|
||||
// editor.setText(...) sets the modified flag to true
|
||||
// but in this case, file is read from disk and hence the contents are not modified at all
|
||||
// so the flag must be changed to unmodified
|
||||
// TODO: Find a better way to check content modification status
|
||||
editor.markUnmodified();
|
||||
postRead();
|
||||
|
||||
selection.validate();
|
||||
editor.validateRange(selection);
|
||||
editor.setSelection(selection);
|
||||
});
|
||||
});
|
||||
|
||||
configureEditorIfNeeded();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Bundle createEditorArgs() {
|
||||
var bundle = new Bundle();
|
||||
if (getFile() == null) {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
bundle.putString(IDEEditor.KEY_FILE, getFile().getAbsolutePath());
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public IDEEditor getEditor() {
|
||||
return binding.editor;
|
||||
}
|
||||
|
||||
protected void postRead() {
|
||||
binding.editor.setupLanguage(getFile());
|
||||
|
||||
binding.editor.setLanguageServer(createLanguageServer(file));
|
||||
|
||||
if (IDELanguageClientImpl.isInitialized()) {
|
||||
binding.editor.setLanguageClient(IDELanguageClientImpl.getInstance());
|
||||
}
|
||||
|
||||
// File must be set only after setting the language server
|
||||
// This will make sure that textDocument/didOpen is sent
|
||||
binding.editor.setFile(getFile());
|
||||
|
||||
if (getContext() instanceof Activity) {
|
||||
((Activity) getContext()).invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private ILanguageServer createLanguageServer(File file) {
|
||||
if (!file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String ext = FileUtils.getFileExtension(file);
|
||||
var serverID = "";
|
||||
switch (ext) {
|
||||
case "java":
|
||||
serverID = JavaLanguageServer.SERVER_ID;
|
||||
break;
|
||||
case "xml":
|
||||
serverID = XMLLanguageServer.SERVER_ID;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return ILanguageServerRegistry.getDefault().getServer(serverID);
|
||||
}
|
||||
|
||||
private void configureEditorIfNeeded() {
|
||||
onCustomFontPrefChanged();
|
||||
onFontSizePrefChanged();
|
||||
onFontLigaturesPrefChanged();
|
||||
onPrintingFlagsPrefChanged();
|
||||
onInputTypePrefChanged();
|
||||
onWordwrapPrefChanged();
|
||||
onMagnifierPrefChanged();
|
||||
onUseIcuPrefChanged();
|
||||
onDeleteEmptyLinesPrefChanged();
|
||||
onDeleteTabsPrefChanged();
|
||||
onStickyScrollEnabeldPrefChanged();
|
||||
}
|
||||
|
||||
protected void onMagnifierPrefChanged() {
|
||||
binding.editor.getComponent(Magnifier.class).setEnabled(getUseMagnifier());
|
||||
}
|
||||
|
||||
protected void onWordwrapPrefChanged() {
|
||||
var enabled = EditorPreferencesKt.getWordwrap();
|
||||
binding.editor.setWordwrap(enabled);
|
||||
}
|
||||
|
||||
protected void onInputTypePrefChanged() {
|
||||
binding.editor.setInputType(IDEEditor.Companion.createInputTypeFlags());
|
||||
}
|
||||
|
||||
protected void onPrintingFlagsPrefChanged() {
|
||||
int flags = 0;
|
||||
if (getDrawLeadingWs()) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_LEADING;
|
||||
}
|
||||
|
||||
if (getDrawTrailingWs()) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_TRAILING;
|
||||
}
|
||||
|
||||
if (getDrawInnerWs()) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_INNER;
|
||||
}
|
||||
|
||||
if (getDrawEmptyLineWs()) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE;
|
||||
}
|
||||
|
||||
if (getDrawLineBreak()) {
|
||||
flags |= IDEEditor.FLAG_DRAW_LINE_SEPARATOR;
|
||||
}
|
||||
|
||||
binding.editor.setNonPrintablePaintingFlags(flags);
|
||||
}
|
||||
|
||||
protected void onFontLigaturesPrefChanged() {
|
||||
var enabled = getFontLigatures();
|
||||
binding.editor.setLigatureEnabled(enabled);
|
||||
}
|
||||
|
||||
protected void onFontSizePrefChanged() {
|
||||
float textSize = getFontSize();
|
||||
if (textSize < 6 || textSize > 32) {
|
||||
textSize = 14;
|
||||
}
|
||||
|
||||
binding.editor.setTextSize(textSize);
|
||||
}
|
||||
|
||||
protected void onUseIcuPrefChanged() {
|
||||
binding.editor.getProps().useICULibToSelectWords = getUseIcu();
|
||||
}
|
||||
|
||||
protected void onCustomFontPrefChanged() {
|
||||
var state = EditorPreferencesKt.getUseCustomFont();
|
||||
binding.editor.setTypefaceText(TypefaceUtilsKt.customOrJBMono(state));
|
||||
binding.editor.setTypefaceLineNumber(TypefaceUtilsKt.customOrJBMono(state));
|
||||
}
|
||||
|
||||
protected void onDeleteEmptyLinesPrefChanged() {
|
||||
binding.editor.getProps().deleteEmptyLineFast = getDeleteEmptyLines();
|
||||
}
|
||||
|
||||
protected void onDeleteTabsPrefChanged() {
|
||||
binding.editor.getProps().deleteMultiSpaces = getDeleteTabsOnBackspace() ? -1 : 1;
|
||||
}
|
||||
|
||||
protected void onStickyScrollEnabeldPrefChanged() {
|
||||
binding.editor.getProps().stickyScroll = getStickyScrollEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only!
|
||||
*
|
||||
* <p>Marks this editor as unmodified. Used only when the activity is being destroyed.
|
||||
*/
|
||||
public void markUnmodified() {
|
||||
binding.editor.markUnmodified();
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only!
|
||||
*
|
||||
* <p>Marks this editor as modified.
|
||||
*/
|
||||
public void markModified() {
|
||||
binding.editor.markModified();
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@SuppressWarnings("unused")
|
||||
public void onPreferenceChanged(PreferenceChangeEvent event) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var prefs = BaseApplication.getBaseInstance().getPrefManager();
|
||||
switch (event.getKey()) {
|
||||
case FONT_SIZE:
|
||||
onFontSizePrefChanged();
|
||||
break;
|
||||
case FONT_LIGATURES:
|
||||
onFontLigaturesPrefChanged();
|
||||
break;
|
||||
case FLAG_LINE_BREAK:
|
||||
case FLAG_WS_INNER:
|
||||
case FLAG_WS_EMPTY_LINE:
|
||||
case FLAG_WS_LEADING:
|
||||
case FLAG_WS_TRAILING:
|
||||
onPrintingFlagsPrefChanged();
|
||||
break;
|
||||
case FLAG_PASSWORD:
|
||||
onInputTypePrefChanged();
|
||||
break;
|
||||
case WORD_WRAP:
|
||||
onWordwrapPrefChanged();
|
||||
break;
|
||||
case USE_MAGNIFER:
|
||||
onMagnifierPrefChanged();
|
||||
break;
|
||||
case USE_ICU:
|
||||
onUseIcuPrefChanged();
|
||||
break;
|
||||
case USE_CUSTOM_FONT:
|
||||
onCustomFontPrefChanged();
|
||||
break;
|
||||
case DELETE_EMPTY_LINES:
|
||||
onDeleteEmptyLinesPrefChanged();
|
||||
break;
|
||||
case DELETE_TABS_ON_BACKSPACE:
|
||||
onDeleteTabsPrefChanged();
|
||||
break;
|
||||
case STICKY_SCROLL_ENABLED:
|
||||
onStickyScrollEnabeldPrefChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean save() {
|
||||
final var file = getFile();
|
||||
if (file == null) {
|
||||
LOG.error("Cannot save file. File instance is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isModified() && file.exists()) {
|
||||
LOG.info(file.getName());
|
||||
LOG.info("File was not modified. Skipping save operation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final var text = getEditor().getText().toString();
|
||||
|
||||
try {
|
||||
FileUtil.writeFile(file, text);
|
||||
notifySaved();
|
||||
return true;
|
||||
} catch (IOException io) {
|
||||
LOG.error("Failed to save file", file, io);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return binding.editor.isModified();
|
||||
}
|
||||
|
||||
private void notifySaved() {
|
||||
binding.editor.dispatchDocumentSaveEvent();
|
||||
}
|
||||
|
||||
public LayoutCodeEditorBinding getBinding() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return binding.editor.getText().toString();
|
||||
}
|
||||
|
||||
public void onEditorSelected() {
|
||||
final var editor = getEditor();
|
||||
if (editor != null) {
|
||||
editor.onEditorSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public void beginSearch() {
|
||||
if (this.binding == null || searchLayout == null) {
|
||||
LOG.warn("Editor layout is null content=" + binding + ", searchLayout=" + searchLayout);
|
||||
return;
|
||||
}
|
||||
|
||||
searchLayout.beginSearchMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this files as saved. Even if it not saved.
|
||||
*/
|
||||
public void markAsSaved() {
|
||||
notifySaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (!EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void updateFile(@NotNull File file) {
|
||||
final var editor = getEditor();
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setFile(file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.ui.editor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.blankj.utilcode.util.SizeUtils
|
||||
import com.itsaky.androidide.app.BaseApplication
|
||||
import com.itsaky.androidide.editor.api.IEditor
|
||||
import com.itsaky.androidide.editor.databinding.LayoutCodeEditorBinding
|
||||
import com.itsaky.androidide.editor.ui.EditorSearchLayout
|
||||
import com.itsaky.androidide.editor.ui.IDEEditor
|
||||
import com.itsaky.androidide.editor.ui.IDEEditor.Companion.createInputTypeFlags
|
||||
import com.itsaky.androidide.editor.utils.ContentReadWrite.readContent
|
||||
import com.itsaky.androidide.editor.utils.ContentReadWrite.writeTo
|
||||
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
|
||||
import com.itsaky.androidide.lsp.IDELanguageClientImpl
|
||||
import com.itsaky.androidide.lsp.api.ILanguageServer
|
||||
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
|
||||
import com.itsaky.androidide.lsp.java.JavaLanguageServer
|
||||
import com.itsaky.androidide.lsp.xml.XMLLanguageServer
|
||||
import com.itsaky.androidide.models.Range
|
||||
import com.itsaky.androidide.preferences.internal.DELETE_EMPTY_LINES
|
||||
import com.itsaky.androidide.preferences.internal.DELETE_TABS_ON_BACKSPACE
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_LINE_BREAK
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_PASSWORD
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_WS_EMPTY_LINE
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_WS_INNER
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_WS_LEADING
|
||||
import com.itsaky.androidide.preferences.internal.FLAG_WS_TRAILING
|
||||
import com.itsaky.androidide.preferences.internal.FONT_LIGATURES
|
||||
import com.itsaky.androidide.preferences.internal.FONT_SIZE
|
||||
import com.itsaky.androidide.preferences.internal.STICKY_SCROLL_ENABLED
|
||||
import com.itsaky.androidide.preferences.internal.USE_CUSTOM_FONT
|
||||
import com.itsaky.androidide.preferences.internal.USE_ICU
|
||||
import com.itsaky.androidide.preferences.internal.USE_MAGNIFER
|
||||
import com.itsaky.androidide.preferences.internal.WORD_WRAP
|
||||
import com.itsaky.androidide.preferences.internal.deleteEmptyLines
|
||||
import com.itsaky.androidide.preferences.internal.deleteTabsOnBackspace
|
||||
import com.itsaky.androidide.preferences.internal.drawEmptyLineWs
|
||||
import com.itsaky.androidide.preferences.internal.drawInnerWs
|
||||
import com.itsaky.androidide.preferences.internal.drawLeadingWs
|
||||
import com.itsaky.androidide.preferences.internal.drawLineBreak
|
||||
import com.itsaky.androidide.preferences.internal.drawTrailingWs
|
||||
import com.itsaky.androidide.preferences.internal.fontLigatures
|
||||
import com.itsaky.androidide.preferences.internal.fontSize
|
||||
import com.itsaky.androidide.preferences.internal.stickyScrollEnabled
|
||||
import com.itsaky.androidide.preferences.internal.useCustomFont
|
||||
import com.itsaky.androidide.preferences.internal.useIcu
|
||||
import com.itsaky.androidide.preferences.internal.useMagnifier
|
||||
import com.itsaky.androidide.preferences.internal.wordwrap
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
|
||||
import com.itsaky.androidide.tasks.cancelIfActive
|
||||
import com.itsaky.androidide.tasks.runOnUiThread
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.customOrJBMono
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
import io.github.rosemoe.sora.text.LineSeparator
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import io.github.rosemoe.sora.widget.component.Magnifier
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* A view that handles opened code editor.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
class CodeEditorView(
|
||||
context: Context,
|
||||
file: File,
|
||||
selection: Range
|
||||
) : LinearLayoutCompat(context), Closeable {
|
||||
|
||||
private var _binding: LayoutCodeEditorBinding? = null
|
||||
private var _searchLayout: EditorSearchLayout? = null
|
||||
|
||||
private val codeEditorScope = CoroutineScope(
|
||||
Dispatchers.Default + CoroutineName("CodeEditorView"))
|
||||
|
||||
private val binding: LayoutCodeEditorBinding
|
||||
get() = checkNotNull(_binding) { "Binding has been destroyed" }
|
||||
|
||||
private val searchLayout: EditorSearchLayout
|
||||
get() = checkNotNull(_searchLayout) { "Search layout has been destroyed" }
|
||||
|
||||
/**
|
||||
* Get the file of this editor.
|
||||
*/
|
||||
val file: File?
|
||||
get() = editor?.file
|
||||
|
||||
/**
|
||||
* Get the [IDEEditor] instance of this editor view.
|
||||
*/
|
||||
val editor: IDEEditor?
|
||||
get() = _binding?.editor
|
||||
|
||||
/**
|
||||
* Returns whether the content of the editor has been modified.
|
||||
*
|
||||
* @see IDEEditor.isModified
|
||||
*/
|
||||
val isModified: Boolean
|
||||
get() = editor?.isModified ?: false
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = ILogger.newInstance("CodeEditorView")
|
||||
}
|
||||
|
||||
init {
|
||||
_binding = LayoutCodeEditorBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
binding.editor.apply {
|
||||
isHighlightCurrentBlock = true
|
||||
props.autoCompletionOnComposing = true
|
||||
dividerWidth = SizeUtils.dp2px(2f).toFloat()
|
||||
colorScheme = SchemeAndroidIDE.newInstance(context)
|
||||
lineSeparator = LineSeparator.LF
|
||||
}
|
||||
|
||||
_searchLayout = EditorSearchLayout(context, binding.editor)
|
||||
|
||||
orientation = VERTICAL
|
||||
|
||||
removeAllViews()
|
||||
addView(binding.root, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f))
|
||||
addView(searchLayout, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
|
||||
readFileAndApplySelection(file, selection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file of this editor. Throws [IllegalStateException] if no file is available.
|
||||
*/
|
||||
fun requireFile(): File {
|
||||
return checkNotNull(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file of this editor. This only updates the file reference of the editor and does
|
||||
* not resets the content.
|
||||
*/
|
||||
fun updateFile(file: File) {
|
||||
val editor = _binding?.editor ?: return
|
||||
editor.file = file
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the editor has been selected and is visible to the user.
|
||||
*/
|
||||
fun onEditorSelected() {
|
||||
_binding?.editor?.onEditorSelected()
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins search mode and shows the [search layout][EditorSearchLayout].
|
||||
*/
|
||||
fun beginSearch() {
|
||||
if (_binding == null || _searchLayout == null) {
|
||||
log.warn(
|
||||
"Editor layout is null content=$binding, searchLayout=$searchLayout")
|
||||
return
|
||||
}
|
||||
|
||||
searchLayout.beginSearchMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this files as saved. Even if it not saved.
|
||||
*/
|
||||
fun markAsSaved() {
|
||||
notifySaved()
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content of the editor to the editor's file.
|
||||
*
|
||||
* @return Whether the save operation was successfully completed or not. If this method returns `false`,
|
||||
* it means that there was an error saving the file or the content of the file was not modified and
|
||||
* hence the save operation was skipped.
|
||||
*/
|
||||
suspend fun save(): Boolean {
|
||||
val file = this.file ?: return false
|
||||
|
||||
if (!isModified && file.exists()) {
|
||||
log.info(file.name)
|
||||
log.info("File was not modified. Skipping save operation.")
|
||||
return false
|
||||
}
|
||||
|
||||
val text = _binding?.editor?.text ?: run {
|
||||
log.error("Failed to save file. Unable to retrieve the content of editor as it is null.")
|
||||
return false
|
||||
}
|
||||
|
||||
disableEditingAndRun(Dispatchers.IO) {
|
||||
// Do not call suspend functions in this scope
|
||||
// the writeTo function acquires lock to the Content object before writing and releases
|
||||
// the lock after writing
|
||||
// if there are any suspend function calls in between, the lock and unlock calls might not
|
||||
// be called on the same thread
|
||||
text.writeTo(file, this@CodeEditorView::updateReadWriteProgress)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_binding?.rwProgress?.isVisible = false
|
||||
}
|
||||
|
||||
notifySaved()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateReadWriteProgress(progress: Int) {
|
||||
val binding = this.binding
|
||||
runOnUiThread {
|
||||
if (binding.rwProgress.isVisible && (progress < 0 || progress >= 100)) {
|
||||
binding.rwProgress.isVisible = false
|
||||
return@runOnUiThread
|
||||
}
|
||||
|
||||
if (!binding.rwProgress.isVisible) {
|
||||
binding.rwProgress.isVisible = true
|
||||
}
|
||||
|
||||
binding.rwProgress.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <R> disableEditingAndRun(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
crossinline action: CoroutineScope.() -> R
|
||||
): R {
|
||||
return withContext(Dispatchers.Main) {
|
||||
_binding?.editor?.isEditable = false
|
||||
|
||||
val result = withContext(context) {
|
||||
action()
|
||||
}
|
||||
|
||||
_binding?.editor?.isEditable = true
|
||||
return@withContext result
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFileAndApplySelection(file: File, selection: Range) {
|
||||
updateReadWriteProgress(0)
|
||||
codeEditorScope.launch {
|
||||
val content = disableEditingAndRun(Dispatchers.IO) {
|
||||
selection.validate()
|
||||
file.readContent(this@CodeEditorView::updateReadWriteProgress)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
initializeContent(content, file, selection)
|
||||
_binding?.rwProgress?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeContent(content: Content, file: File, selection: Range) {
|
||||
binding.editor.apply {
|
||||
val args = Bundle().apply {
|
||||
putString(IEditor.KEY_FILE, file.absolutePath)
|
||||
}
|
||||
|
||||
setText(content, args)
|
||||
|
||||
// editor.setText(...) sets the modified flag to true
|
||||
// but in this case, file is read from disk and hence the contents are not modified at all
|
||||
// so the flag must be changed to unmodified
|
||||
// TODO: Find a better way to check content modification status
|
||||
markUnmodified()
|
||||
postRead(file)
|
||||
|
||||
validateRange(selection)
|
||||
setSelection(selection)
|
||||
}
|
||||
|
||||
configureEditorIfNeeded()
|
||||
}
|
||||
|
||||
private fun postRead(file: File) {
|
||||
binding.editor.setupLanguage(file)
|
||||
binding.editor.setLanguageServer(createLanguageServer(file))
|
||||
|
||||
if (IDELanguageClientImpl.isInitialized()) {
|
||||
binding.editor.setLanguageClient(IDELanguageClientImpl.getInstance())
|
||||
}
|
||||
|
||||
// File must be set only after setting the language server
|
||||
// This will make sure that textDocument/didOpen is sent
|
||||
binding.editor.file = file
|
||||
|
||||
if (context is Activity) {
|
||||
(context as Activity).invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLanguageServer(file: File): ILanguageServer? {
|
||||
if (!file.isFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
val serverID: String = when (file.extension) {
|
||||
"java" -> JavaLanguageServer.SERVER_ID
|
||||
"xml" -> XMLLanguageServer.SERVER_ID
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return ILanguageServerRegistry.getDefault().getServer(serverID)
|
||||
}
|
||||
|
||||
private fun configureEditorIfNeeded() {
|
||||
onCustomFontPrefChanged()
|
||||
onFontSizePrefChanged()
|
||||
onFontLigaturesPrefChanged()
|
||||
onPrintingFlagsPrefChanged()
|
||||
onInputTypePrefChanged()
|
||||
onWordwrapPrefChanged()
|
||||
onMagnifierPrefChanged()
|
||||
onUseIcuPrefChanged()
|
||||
onDeleteEmptyLinesPrefChanged()
|
||||
onDeleteTabsPrefChanged()
|
||||
onStickyScrollEnabeldPrefChanged()
|
||||
}
|
||||
|
||||
private fun onMagnifierPrefChanged() {
|
||||
binding.editor.getComponent(Magnifier::class.java).isEnabled = useMagnifier
|
||||
}
|
||||
|
||||
private fun onWordwrapPrefChanged() {
|
||||
val enabled = wordwrap
|
||||
binding.editor.isWordwrap = enabled
|
||||
}
|
||||
|
||||
private fun onInputTypePrefChanged() {
|
||||
binding.editor.inputType = createInputTypeFlags()
|
||||
}
|
||||
|
||||
private fun onPrintingFlagsPrefChanged() {
|
||||
var flags = 0
|
||||
if (drawLeadingWs) {
|
||||
flags = flags or CodeEditor.FLAG_DRAW_WHITESPACE_LEADING
|
||||
}
|
||||
if (drawTrailingWs) {
|
||||
flags = flags or CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING
|
||||
}
|
||||
if (drawInnerWs) {
|
||||
flags = flags or CodeEditor.FLAG_DRAW_WHITESPACE_INNER
|
||||
}
|
||||
if (drawEmptyLineWs) {
|
||||
flags = flags or CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE
|
||||
}
|
||||
if (drawLineBreak) {
|
||||
flags = flags or CodeEditor.FLAG_DRAW_LINE_SEPARATOR
|
||||
}
|
||||
binding.editor.nonPrintablePaintingFlags = flags
|
||||
}
|
||||
|
||||
private fun onFontLigaturesPrefChanged() {
|
||||
val enabled = fontLigatures
|
||||
binding.editor.isLigatureEnabled = enabled
|
||||
}
|
||||
|
||||
private fun onFontSizePrefChanged() {
|
||||
var textSize = fontSize
|
||||
if (textSize < 6 || textSize > 32) {
|
||||
textSize = 14f
|
||||
}
|
||||
binding.editor.setTextSize(textSize)
|
||||
}
|
||||
|
||||
private fun onUseIcuPrefChanged() {
|
||||
binding.editor.props.useICULibToSelectWords = useIcu
|
||||
}
|
||||
|
||||
private fun onCustomFontPrefChanged() {
|
||||
val state = useCustomFont
|
||||
binding.editor.typefaceText = customOrJBMono(state)
|
||||
binding.editor.typefaceLineNumber = customOrJBMono(state)
|
||||
}
|
||||
|
||||
private fun onDeleteEmptyLinesPrefChanged() {
|
||||
binding.editor.props.deleteEmptyLineFast = deleteEmptyLines
|
||||
}
|
||||
|
||||
private fun onDeleteTabsPrefChanged() {
|
||||
binding.editor.props.deleteMultiSpaces = if (deleteTabsOnBackspace) -1 else 1
|
||||
}
|
||||
|
||||
private fun onStickyScrollEnabeldPrefChanged() {
|
||||
binding.editor.props.stickyScroll = stickyScrollEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only!
|
||||
*
|
||||
*
|
||||
* Marks this editor as unmodified. Used only when the activity is being destroyed.
|
||||
*/
|
||||
internal fun markUnmodified() {
|
||||
binding.editor.markUnmodified()
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only!
|
||||
*
|
||||
*
|
||||
* Marks this editor as modified.
|
||||
*/
|
||||
internal fun markModified() {
|
||||
binding.editor.markModified()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun onPreferenceChanged(event: PreferenceChangeEvent) {
|
||||
if (_binding == null) {
|
||||
return
|
||||
}
|
||||
|
||||
BaseApplication.getBaseInstance().prefManager
|
||||
when (event.key) {
|
||||
FONT_SIZE -> onFontSizePrefChanged()
|
||||
FONT_LIGATURES -> onFontLigaturesPrefChanged()
|
||||
|
||||
FLAG_LINE_BREAK,
|
||||
FLAG_WS_INNER,
|
||||
FLAG_WS_EMPTY_LINE,
|
||||
FLAG_WS_LEADING,
|
||||
FLAG_WS_TRAILING -> onPrintingFlagsPrefChanged()
|
||||
|
||||
FLAG_PASSWORD -> onInputTypePrefChanged()
|
||||
WORD_WRAP -> onWordwrapPrefChanged()
|
||||
USE_MAGNIFER -> onMagnifierPrefChanged()
|
||||
USE_ICU -> onUseIcuPrefChanged()
|
||||
USE_CUSTOM_FONT -> onCustomFontPrefChanged()
|
||||
DELETE_EMPTY_LINES -> onDeleteEmptyLinesPrefChanged()
|
||||
DELETE_TABS_ON_BACKSPACE -> onDeleteTabsPrefChanged()
|
||||
STICKY_SCROLL_ENABLED -> onStickyScrollEnabeldPrefChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the editor that its content has been saved.
|
||||
*/
|
||||
private fun notifySaved() {
|
||||
binding.editor.dispatchDocumentSaveEvent()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (!EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
codeEditorScope.cancelIfActive("Cancellation was requested")
|
||||
_binding?.editor?.apply {
|
||||
notifyClose()
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
android:elevation="10dp" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/build_progress_indicator"
|
||||
android:id="@+id/progress_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true" />
|
||||
|
||||
@@ -17,11 +17,18 @@
|
||||
|
||||
package com.itsaky.androidide.tasks
|
||||
|
||||
import com.itsaky.androidide.flashbar.Flashbar
|
||||
import com.itsaky.androidide.progress.ICancelChecker
|
||||
import com.itsaky.androidide.utils.flashProgress
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.InterruptedIOException
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* Calls [CoroutineScope.cancel] only if a job is active in the scope.
|
||||
@@ -43,6 +50,50 @@ fun CoroutineScope.cancelIfActive(exception: CancellationException? = null) {
|
||||
job?.cancel(exception)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a new coroutine without blocking the current thread. This method shows a progress
|
||||
* indicator using [Flashbar] while the [action] is being executed. The [Flashbar] is automatically
|
||||
* dismissed after the action completes, no matter if it fails or succeeds.
|
||||
*
|
||||
* @param configureFlashbar A function to configure the [Flashbar.Builder] as necessary.
|
||||
* @param invokeOnCompletion A function which is called when the job completes, fails or is cancelled.
|
||||
* This function must follow [Job.invokeOnCompletion]'s contract.
|
||||
* @param action The action to be executed.
|
||||
* @see CoroutineScope.launch
|
||||
*/
|
||||
fun CoroutineScope.launchAsyncWithProgress(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
configureFlashbar: ((Flashbar.Builder, ICancelChecker) -> Unit)? = null,
|
||||
invokeOnCompletion: ((Throwable?) -> Unit)? = null,
|
||||
action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit
|
||||
): Job? {
|
||||
|
||||
val cancelChecker = object : ICancelChecker.Default() {
|
||||
var job: Job? = null
|
||||
|
||||
override fun cancel() {
|
||||
job?.cancel("Cancelled by user")
|
||||
job = null
|
||||
super.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
return flashProgress({
|
||||
configureFlashbar?.invoke(this, cancelChecker)
|
||||
}) { flashbar ->
|
||||
return@flashProgress launch(context = context, start = start) {
|
||||
cancelChecker.job = coroutineContext[Job]
|
||||
action(flashbar, cancelChecker)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { throwable ->
|
||||
runOnUiThread { flashbar.dismiss() }
|
||||
invokeOnCompletion?.invoke(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given [action] and re-throws the [Throwable] if this [Throwable] is thrown for
|
||||
* cancellation or interruption purposes.
|
||||
|
||||
@@ -104,10 +104,10 @@ fun Activity.flashInfo(@StringRes msg: Int) {
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun <R> Activity.flashProgress(
|
||||
fun <R : Any?> Activity.flashProgress(
|
||||
configure: (Flashbar.Builder.() -> Unit)? = null,
|
||||
action: (Flashbar) -> R?
|
||||
) : R? {
|
||||
action: (Flashbar) -> R
|
||||
) : R {
|
||||
val builder = flashbarBuilder(gravity = TOP, duration = DURATION_INDEFINITE)
|
||||
.showProgress(Flashbar.ProgressPosition.LEFT)
|
||||
|
||||
|
||||
@@ -60,8 +60,11 @@ fun flashInfo(@StringRes msg: Int) {
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun flashProgress(configure: Consumer<Flashbar.Builder>? = null, action: Consumer<Flashbar>) {
|
||||
withActivity { flashProgress(configure, action) }
|
||||
fun <R> flashProgress(
|
||||
configure: (Flashbar.Builder.() -> Unit)? = null,
|
||||
action: (Flashbar) -> R?
|
||||
) : R? {
|
||||
return withActivity { flashProgress(configure, action) }
|
||||
}
|
||||
|
||||
private fun <T> withActivity(action: Activity.() -> T?): T? {
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
package com.itsaky.androidide.editor.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Debug
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
@@ -72,26 +72,24 @@ import com.itsaky.androidide.projects.FileManager.onDocumentOpen
|
||||
import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
|
||||
import com.itsaky.androidide.tasks.cancelIfActive
|
||||
import com.itsaky.androidide.tasks.launchAsyncWithProgress
|
||||
import com.itsaky.androidide.utils.DocumentUtils
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import com.itsaky.androidide.utils.flashProgress
|
||||
import io.github.rosemoe.sora.event.ContentChangeEvent
|
||||
import io.github.rosemoe.sora.event.SelectionChangeEvent
|
||||
import io.github.rosemoe.sora.lang.EmptyLanguage
|
||||
import io.github.rosemoe.sora.lang.Language
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import io.github.rosemoe.sora.widget.EditorSearcher
|
||||
import io.github.rosemoe.sora.widget.IDEEditorSearcher
|
||||
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion
|
||||
import io.github.rosemoe.sora.widget.component.EditorBuiltinComponent
|
||||
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -201,6 +199,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* Set the file for this editor.
|
||||
*/
|
||||
fun setFile(file: File?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
this.file = file
|
||||
file?.also {
|
||||
dispatchDocumentOpenEvent()
|
||||
@@ -212,10 +214,16 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun setSelection(position: Position) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
setSelection(position.line, position.column)
|
||||
}
|
||||
|
||||
override fun setSelection(start: Position, end: Position) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (!isValidPosition(start, true) || !isValidPosition(end, true)) {
|
||||
log.warn("Invalid selection range: start=$start end=$end")
|
||||
return
|
||||
@@ -238,6 +246,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun validateRange(range: Range) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val start = range.start
|
||||
val end = range.end
|
||||
val text = text
|
||||
@@ -251,6 +262,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun isValidRange(range: Range?, allowColumnEqual: Boolean): Boolean {
|
||||
if (isReleased) {
|
||||
return false
|
||||
}
|
||||
if (range == null) {
|
||||
return false
|
||||
}
|
||||
@@ -262,6 +276,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun isValidPosition(position: Position?, allowColumnEqual: Boolean): Boolean {
|
||||
if (isReleased) {
|
||||
return false
|
||||
}
|
||||
return if (position == null) {
|
||||
false
|
||||
} else isValidLine(position.line) &&
|
||||
@@ -269,6 +286,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun isValidLine(line: Int): Boolean {
|
||||
if (isReleased) {
|
||||
return false
|
||||
}
|
||||
return line >= 0 && line < text.lineCount
|
||||
}
|
||||
|
||||
@@ -278,6 +298,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun append(text: CharSequence?): Int {
|
||||
if (isReleased) {
|
||||
return 0
|
||||
}
|
||||
val content = getText()
|
||||
if (lineCount <= 0) {
|
||||
return 0
|
||||
@@ -293,17 +316,26 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
|
||||
@UiThread
|
||||
override fun replaceContent(newContent: CharSequence?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val lastLine = text.lineCount - 1
|
||||
val lastColumn = text.getColumnCount(lastLine)
|
||||
text.replace(0, 0, lastLine, lastColumn, newContent ?: "")
|
||||
}
|
||||
|
||||
override fun goToEnd() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val line = text.lineCount - 1
|
||||
setSelection(line, 0)
|
||||
}
|
||||
|
||||
override fun setLanguageServer(server: ILanguageServer?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
this.languageServer = server
|
||||
server?.also {
|
||||
this.languageClient = it.client
|
||||
@@ -315,10 +347,16 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun setLanguageClient(client: ILanguageClient?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
this.languageClient = client
|
||||
}
|
||||
|
||||
override fun executeCommand(command: Command?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (command == null) {
|
||||
log.warn("Cannot execute command in editor. Command is null.")
|
||||
return
|
||||
@@ -337,6 +375,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun signatureHelp() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val languageServer = this.languageServer ?: return
|
||||
val file = this.file ?: return
|
||||
|
||||
@@ -353,42 +394,52 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun showSignatureHelp(help: SignatureHelp?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
signatureHelpWindow.setupAndDisplay(help)
|
||||
}
|
||||
|
||||
override fun findDefinition() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val languageServer = this.languageServer ?: return
|
||||
val file = file ?: return
|
||||
|
||||
launchAsyncWithProgress(string.msg_finding_definition) { flashbar, cancelChecker ->
|
||||
launchCancellableAsyncWithProgress(string.msg_finding_definition) { _, cancelChecker ->
|
||||
val params = DefinitionParams(file.toPath(), cursorLSPPosition)
|
||||
val result = languageServer.findDefinition(params, cancelChecker)
|
||||
onFindDefinitionResult(flashbar, result)
|
||||
onFindDefinitionResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findReferences() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val languageServer = this.languageServer ?: return
|
||||
val file = file ?: return
|
||||
|
||||
launchAsyncWithProgress(string.msg_finding_references) { flashbar, cancelChecker ->
|
||||
delay(3000L)
|
||||
launchCancellableAsyncWithProgress(string.msg_finding_references) { _, cancelChecker ->
|
||||
val params = ReferenceParams(file.toPath(), cursorLSPPosition, true)
|
||||
val result = languageServer.findReferences(params, cancelChecker)
|
||||
onFindReferencesResult(flashbar, result)
|
||||
onFindReferencesResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun expandSelection() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val languageServer = this.languageServer ?: return
|
||||
val file = file ?: return
|
||||
|
||||
launchAsyncWithProgress(string.please_wait) { flashbar, _ ->
|
||||
launchCancellableAsyncWithProgress(string.please_wait) { _, _ ->
|
||||
val params = ExpandSelectionParams(file.toPath(), cursorLSPRange)
|
||||
val result = languageServer.expandSelection(params)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
flashbar.dismiss()
|
||||
setSelection(result)
|
||||
}
|
||||
}
|
||||
@@ -423,6 +474,11 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
|
||||
override fun release() {
|
||||
ensureWindowsDismissed()
|
||||
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
super.release()
|
||||
|
||||
snippetController.apply {
|
||||
@@ -434,10 +490,21 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
_actionsMenu?.destroy()
|
||||
|
||||
_actionsMenu = null
|
||||
_signatureHelpWindow = null
|
||||
_diagnosticWindow = null
|
||||
|
||||
languageServer = null
|
||||
languageClient = null
|
||||
|
||||
file = null
|
||||
fileVersion = 0
|
||||
markUnmodified()
|
||||
|
||||
selectionChangeRunner?.also { selectionChangeHandler.removeCallbacks(it) }
|
||||
selectionChangeRunner = null
|
||||
|
||||
if (EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
@@ -481,6 +548,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* Analyze the opened file and publish the diagnostics result.
|
||||
*/
|
||||
open fun analyze() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (editorLanguage !is IDELanguage) {
|
||||
return
|
||||
}
|
||||
@@ -512,6 +582,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* Notify the language server that the file in this editor is about to be closed.
|
||||
*/
|
||||
open fun notifyClose() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
file ?: run {
|
||||
log.info("Cannot notify language server. File is null.")
|
||||
return
|
||||
@@ -533,6 +606,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* Called when this editor is selected and visible to the user.
|
||||
*/
|
||||
open fun onEditorSelected() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
file ?: return
|
||||
dispatchDocumentSelectedEvent()
|
||||
}
|
||||
@@ -542,6 +619,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
*/
|
||||
open fun dispatchDocumentSaveEvent() {
|
||||
markUnmodified()
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (getFile() == null) {
|
||||
return
|
||||
}
|
||||
@@ -566,6 +646,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* This applies a proper [Language] and the color scheme to the editor.
|
||||
*/
|
||||
open fun setupLanguage(file: File?) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
@@ -595,6 +678,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
type: String,
|
||||
scheme: SchemeAndroidIDE?
|
||||
) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
var finalScheme = if (scheme != null) {
|
||||
scheme
|
||||
} else {
|
||||
@@ -634,13 +720,6 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file for this editor. This does not dispatch the [DocumentOpenEvent].
|
||||
*/
|
||||
open fun updateFile(file: File?) {
|
||||
this.file = file
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the editor.
|
||||
*/
|
||||
@@ -662,6 +741,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
getComponent(EditorTextActionWindow::class.java).isEnabled = false
|
||||
|
||||
subscribeEvent(ContentChangeEvent::class.java) { event, _ ->
|
||||
if (isReleased) {
|
||||
return@subscribeEvent
|
||||
}
|
||||
|
||||
markModified()
|
||||
file ?: return@subscribeEvent
|
||||
|
||||
@@ -672,6 +755,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
subscribeEvent(SelectionChangeEvent::class.java) { _, _ ->
|
||||
if (isReleased) {
|
||||
return@subscribeEvent
|
||||
}
|
||||
|
||||
if (_diagnosticWindow?.isShowing == true) {
|
||||
_diagnosticWindow?.dismiss()
|
||||
}
|
||||
@@ -685,36 +772,26 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
protected open fun launchAsyncWithProgress(
|
||||
message: Int,
|
||||
protected open fun launchCancellableAsyncWithProgress(
|
||||
@StringRes message: Int,
|
||||
action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit
|
||||
): Job {
|
||||
val cancelChecker = object : ICancelChecker.Default() {
|
||||
var job: Job? = null
|
||||
|
||||
override fun cancel() {
|
||||
job?.cancel("Cancelled by user")
|
||||
job = null
|
||||
super.cancel()
|
||||
}
|
||||
): Job? {
|
||||
if (isReleased) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (context as Activity).flashProgress({
|
||||
configureFlashbar(this, string.msg_finding_definition, cancelChecker)
|
||||
}) { flashbar ->
|
||||
return@flashProgress editorScope.launch {
|
||||
cancelChecker.job = coroutineContext[Job]
|
||||
action(flashbar, cancelChecker)
|
||||
}
|
||||
}!!
|
||||
return editorScope.launchAsyncWithProgress(configureFlashbar = { builder, cancelChecker ->
|
||||
configureFlashbar(builder, message, cancelChecker)
|
||||
}, action = action)
|
||||
}
|
||||
|
||||
protected open suspend fun onFindDefinitionResult(
|
||||
flashbar: Flashbar,
|
||||
result: DefinitionResult?,
|
||||
) = withContext(Dispatchers.Main) {
|
||||
|
||||
flashbar.dismiss()
|
||||
if (isReleased) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val languageClient = languageClient ?: run {
|
||||
log.error("No language client found to handle the definitions result")
|
||||
@@ -751,12 +828,12 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
protected open suspend fun onFindReferencesResult(
|
||||
flashbar: Flashbar,
|
||||
result: ReferenceResult?
|
||||
) = withContext(Dispatchers.Main) {
|
||||
flashbar.dismiss()
|
||||
|
||||
flashbar.dismiss()
|
||||
if (isReleased) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val languageClient = languageClient ?: run {
|
||||
log.error("No language client found to handle the references result")
|
||||
@@ -789,6 +866,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
protected open fun dispatchDocumentOpenEvent() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
val file = this.file ?: return
|
||||
|
||||
this.fileVersion = 0
|
||||
@@ -805,6 +886,10 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
protected open fun dispatchDocumentChangeEvent(event: ContentChangeEvent) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
|
||||
val file = file?.toPath() ?: return
|
||||
var type = ChangeType.INSERT
|
||||
if (event.action == ContentChangeEvent.ACTION_DELETE) {
|
||||
@@ -830,12 +915,18 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
protected open fun dispatchDocumentSelectedEvent() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val file = file ?: return
|
||||
val selectedEvent = DocumentSelectedEvent(file.toPath())
|
||||
EventBus.getDefault().post(selectedEvent)
|
||||
}
|
||||
|
||||
protected open fun dispatchDocumentCloseEvent() {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
val file = file ?: return
|
||||
val closeEvent = DocumentCloseEvent(file.toPath(), cursorLSPRange)
|
||||
|
||||
@@ -856,6 +947,9 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
* @param event The content change event.
|
||||
*/
|
||||
private fun checkForSignatureHelp(event: ContentChangeEvent) {
|
||||
if (isReleased) {
|
||||
return
|
||||
}
|
||||
if (languageServer == null) {
|
||||
return
|
||||
}
|
||||
@@ -883,10 +977,4 @@ open class IDEEditor @JvmOverloads constructor(
|
||||
bar.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Flashbar.dismissOnUiThread() {
|
||||
withContext(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.editor.utils
|
||||
|
||||
import io.github.rosemoe.sora.text.CharArrayWrapper
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
import io.github.rosemoe.sora.text.ContentLockAccessor
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Utility methods for reading and writing [Content] objects. This class is similar to
|
||||
* [ContentIO][io.github.rosemoe.sora.text.ContentIO] but provides ability to track the progress.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object ContentReadWrite {
|
||||
|
||||
/**
|
||||
* Write this [Content] to the given [File].
|
||||
*
|
||||
* @param progressConsumer A function which is invoked to notify about the write progress.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun Content.writeTo(file: File, progressConsumer: ((Int) -> Unit)? = null) {
|
||||
val consumer = discreteProgressConsumer(progressConsumer = progressConsumer)
|
||||
|
||||
file.writer().buffered(DEFAULT_BUFFER_SIZE * 2).use { writer ->
|
||||
val lastLine = lineCount - 1
|
||||
val length = length
|
||||
|
||||
ContentLockAccessor.lock(this, false)
|
||||
var totalWrote = 0.0
|
||||
try {
|
||||
for (lineIdx in 0..lastLine) {
|
||||
val line = getLine(lineIdx)
|
||||
writer.write(line.value, 0, line.length)
|
||||
|
||||
val separatorChars = line.lineSeparator.chars
|
||||
writer.write(separatorChars)
|
||||
|
||||
totalWrote += line.length + separatorChars.size
|
||||
val saveProgress = (totalWrote / length) * 100
|
||||
consumer(floor(saveProgress).toInt())
|
||||
}
|
||||
} catch (err: IOException) {
|
||||
throw RuntimeException("Failed to write editor's content to file: ${file.absolutePath}",
|
||||
err)
|
||||
} finally {
|
||||
ContentLockAccessor.unlock(this, false)
|
||||
consumer(100)
|
||||
}
|
||||
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads this file's content to a new [Content] object.
|
||||
*
|
||||
* @param progressConsumer A function to consume the read progress.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun File.readContent(progressConsumer: ((Int) -> Unit)? = null): Content {
|
||||
val consumer = discreteProgressConsumer(progressConsumer = progressConsumer)
|
||||
return Content().apply {
|
||||
isUndoEnabled = false
|
||||
val input = inputStream()
|
||||
val total = input.available().let { if (it == 0) 1 else it } // avoid divide by 0
|
||||
input.reader().use { reader ->
|
||||
val buffer = CharArray(DEFAULT_BUFFER_SIZE * 2)
|
||||
val wrapper = CharArrayWrapper(buffer, 0)
|
||||
var totalRead = 0.0
|
||||
var count: Int
|
||||
while (true) {
|
||||
count = reader.read(buffer)
|
||||
if (count == -1) {
|
||||
break
|
||||
}
|
||||
if (count == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
totalRead += count
|
||||
|
||||
val progress = floor((totalRead / total) * 100).toInt()
|
||||
|
||||
if (buffer[count - 1] == '\r') {
|
||||
val peek = reader.read()
|
||||
if (peek == '\n'.code) {
|
||||
wrapper.setDataCount(count - 1)
|
||||
var line = lineCount - 1
|
||||
insert(line, getColumnCount(line), wrapper)
|
||||
|
||||
line = lineCount - 1
|
||||
insert(line, getColumnCount(line), "\r\n")
|
||||
consumer(progress)
|
||||
continue
|
||||
|
||||
} else if (peek != -1) {
|
||||
wrapper.setDataCount(count)
|
||||
var line = lineCount - 1
|
||||
insert(line, getColumnCount(line), wrapper)
|
||||
|
||||
line = lineCount - 1
|
||||
insert(line, getColumnCount(line), peek.toChar().toString())
|
||||
consumer(progress)
|
||||
continue
|
||||
}
|
||||
}
|
||||
wrapper.setDataCount(count)
|
||||
|
||||
val line = lineCount - 1
|
||||
insert(line, getColumnCount(line), wrapper)
|
||||
|
||||
consumer(progress)
|
||||
}
|
||||
}
|
||||
isUndoEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun discreteProgressConsumer(
|
||||
stepSize: Int = 5,
|
||||
progressConsumer: ((Int) -> Unit)?
|
||||
) : (Int) -> Unit {
|
||||
var lastProgress = -1
|
||||
val consumer = fun (progress: Int) {
|
||||
if (lastProgress == -1 || progress >= 100 || progress - lastProgress >= stepSize) {
|
||||
progressConsumer?.invoke(progress)
|
||||
lastProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
return consumer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.github.rosemoe.sora.text
|
||||
|
||||
/**
|
||||
* Provides access to [Content.lock] and [Content.unlock].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object ContentLockAccessor {
|
||||
|
||||
@JvmStatic
|
||||
fun lock(content: Content, write: Boolean) {
|
||||
content.lock(write)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun unlock(content: Content, write: Boolean) {
|
||||
content.unlock(write)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ This file is part of AndroidIDE.
|
||||
~
|
||||
~ AndroidIDE is free software: you can redistribute it and/or modify
|
||||
@@ -16,35 +15,31 @@
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.itsaky.androidide.editor.ui.IDEEditor
|
||||
android:id="@+id/editor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/diagnostic_text_container"
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/rw_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true">
|
||||
android:indeterminate="false"
|
||||
android:max="100"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:progress="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/diagnostic_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:ellipsize="end"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
<com.itsaky.androidide.editor.ui.IDEEditor
|
||||
android:id="@+id/editor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/rw_progress" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -46,7 +46,8 @@ private const val DEFAUT_ICON_SCALE = 1.0f
|
||||
class Flashbar private constructor(private var builder: Builder) {
|
||||
|
||||
private lateinit var flashbarContainerView: FlashbarContainerView
|
||||
private lateinit var flashbarView: FlashbarView
|
||||
|
||||
lateinit var flashbarView: FlashbarView
|
||||
|
||||
/** Shows a flashbar */
|
||||
fun show() {
|
||||
|
||||
@@ -62,7 +62,7 @@ import com.itsaky.androidide.flashbar.view.ShadowView
|
||||
* It can either be present at the top or at the bottom of the screen. It will always consume touch
|
||||
* events and respond as necessary.
|
||||
*/
|
||||
internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
|
||||
private val compensationMarginTop =
|
||||
resources.getDimension(R.dimen.fb_top_compensation_margin).toInt()
|
||||
@@ -166,140 +166,140 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.parentFlashbarContainer = flashbarContainerView
|
||||
}
|
||||
|
||||
internal fun setBarBackgroundDrawable(drawable: Drawable?) {
|
||||
fun setBarBackgroundDrawable(drawable: Drawable?) {
|
||||
if (drawable == null) return
|
||||
this.fbRoot.background = drawable
|
||||
}
|
||||
|
||||
internal fun setBarBackgroundColor(@ColorInt color: Int?) {
|
||||
fun setBarBackgroundColor(@ColorInt color: Int?) {
|
||||
if (color == null) return
|
||||
this.fbRoot.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
internal fun setBarTapListener(listener: OnTapListener?) {
|
||||
fun setBarTapListener(listener: OnTapListener?) {
|
||||
if (listener == null) return
|
||||
|
||||
this.fbRoot.setOnClickListener { listener.onTap(parentFlashbarContainer.parentFlashbar) }
|
||||
}
|
||||
|
||||
internal fun setTitle(title: String?) {
|
||||
fun setTitle(title: String?) {
|
||||
if (TextUtils.isEmpty(title)) return
|
||||
|
||||
this.fbTitle.text = title
|
||||
this.fbTitle.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setTitleSpanned(title: Spanned?) {
|
||||
fun setTitleSpanned(title: Spanned?) {
|
||||
if (title == null) return
|
||||
|
||||
this.fbTitle.text = title
|
||||
this.fbTitle.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setTitleTypeface(typeface: Typeface?) {
|
||||
fun setTitleTypeface(typeface: Typeface?) {
|
||||
if (typeface == null) return
|
||||
fbTitle.typeface = typeface
|
||||
}
|
||||
|
||||
internal fun setTitleSizeInPx(size: Float?) {
|
||||
fun setTitleSizeInPx(size: Float?) {
|
||||
if (size == null) return
|
||||
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
|
||||
}
|
||||
|
||||
internal fun setTitleSizeInSp(size: Float?) {
|
||||
fun setTitleSizeInSp(size: Float?) {
|
||||
if (size == null) return
|
||||
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
}
|
||||
|
||||
internal fun setTitleColor(color: Int?) {
|
||||
fun setTitleColor(color: Int?) {
|
||||
if (color == null) return
|
||||
fbTitle.setTextColor(color)
|
||||
}
|
||||
|
||||
internal fun setTitleAppearance(titleAppearance: Int?) {
|
||||
fun setTitleAppearance(titleAppearance: Int?) {
|
||||
if (titleAppearance == null) return
|
||||
this.fbTitle.setTextAppearance(titleAppearance)
|
||||
}
|
||||
|
||||
internal fun setMessage(message: String?) {
|
||||
fun setMessage(message: String?) {
|
||||
if (TextUtils.isEmpty(message)) return
|
||||
|
||||
this.fbMessage.text = message
|
||||
this.fbMessage.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setMessageSpanned(message: Spanned?) {
|
||||
fun setMessageSpanned(message: Spanned?) {
|
||||
if (message == null) return
|
||||
|
||||
this.fbMessage.text = message
|
||||
this.fbMessage.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setMessageTypeface(typeface: Typeface?) {
|
||||
fun setMessageTypeface(typeface: Typeface?) {
|
||||
if (typeface == null) return
|
||||
this.fbMessage.typeface = typeface
|
||||
}
|
||||
|
||||
internal fun setMessageSizeInPx(size: Float?) {
|
||||
fun setMessageSizeInPx(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
|
||||
}
|
||||
|
||||
internal fun setMessageSizeInSp(size: Float?) {
|
||||
fun setMessageSizeInSp(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
}
|
||||
|
||||
internal fun setMessageColor(color: Int?) {
|
||||
fun setMessageColor(color: Int?) {
|
||||
if (color == null) return
|
||||
this.fbMessage.setTextColor(color)
|
||||
}
|
||||
|
||||
internal fun setMessageAppearance(messageAppearance: Int?) {
|
||||
fun setMessageAppearance(messageAppearance: Int?) {
|
||||
if (messageAppearance == null) return
|
||||
this.fbMessage.setTextAppearance(messageAppearance)
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionText(text: String?) {
|
||||
fun setPrimaryActionText(text: String?) {
|
||||
if (TextUtils.isEmpty(text)) return
|
||||
|
||||
this.fbPrimaryAction.text = text
|
||||
this.fbPrimaryAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextSpanned(text: Spanned?) {
|
||||
fun setPrimaryActionTextSpanned(text: Spanned?) {
|
||||
if (text == null) return
|
||||
|
||||
this.fbPrimaryAction.text = text
|
||||
this.fbPrimaryAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextTypeface(typeface: Typeface?) {
|
||||
fun setPrimaryActionTextTypeface(typeface: Typeface?) {
|
||||
if (typeface == null) return
|
||||
this.fbPrimaryAction.typeface = typeface
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextSizeInPx(size: Float?) {
|
||||
fun setPrimaryActionTextSizeInPx(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextSizeInSp(size: Float?) {
|
||||
fun setPrimaryActionTextSizeInSp(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextColor(color: Int?) {
|
||||
fun setPrimaryActionTextColor(color: Int?) {
|
||||
if (color == null) return
|
||||
this.fbPrimaryAction.setTextColor(color)
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTextAppearance(messageAppearance: Int?) {
|
||||
fun setPrimaryActionTextAppearance(messageAppearance: Int?) {
|
||||
if (messageAppearance == null) return
|
||||
this.fbPrimaryAction.setTextAppearance(messageAppearance)
|
||||
}
|
||||
|
||||
internal fun setPrimaryActionTapListener(listener: OnActionTapListener?) {
|
||||
fun setPrimaryActionTapListener(listener: OnActionTapListener?) {
|
||||
if (listener == null) return
|
||||
|
||||
this.fbPrimaryAction.setOnClickListener {
|
||||
@@ -307,7 +307,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setPositiveActionText(text: String?) {
|
||||
fun setPositiveActionText(text: String?) {
|
||||
if (TextUtils.isEmpty(text)) return
|
||||
|
||||
this.fbSecondaryActionContainer.visibility = VISIBLE
|
||||
@@ -315,7 +315,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.fbPositiveAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextSpanned(text: Spanned?) {
|
||||
fun setPositiveActionTextSpanned(text: Spanned?) {
|
||||
if (text == null) return
|
||||
|
||||
this.fbSecondaryActionContainer.visibility = VISIBLE
|
||||
@@ -323,32 +323,32 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.fbPositiveAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextTypeface(typeface: Typeface?) {
|
||||
fun setPositiveActionTextTypeface(typeface: Typeface?) {
|
||||
if (typeface == null) return
|
||||
this.fbPositiveAction.typeface = typeface
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextSizeInPx(size: Float?) {
|
||||
fun setPositiveActionTextSizeInPx(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextSizeInSp(size: Float?) {
|
||||
fun setPositiveActionTextSizeInSp(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextColor(color: Int?) {
|
||||
fun setPositiveActionTextColor(color: Int?) {
|
||||
if (color == null) return
|
||||
this.fbPositiveAction.setTextColor(color)
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTextAppearance(messageAppearance: Int?) {
|
||||
fun setPositiveActionTextAppearance(messageAppearance: Int?) {
|
||||
if (messageAppearance == null) return
|
||||
this.fbPositiveAction.setTextAppearance(messageAppearance)
|
||||
}
|
||||
|
||||
internal fun setPositiveActionTapListener(listener: OnActionTapListener?) {
|
||||
fun setPositiveActionTapListener(listener: OnActionTapListener?) {
|
||||
if (listener == null) return
|
||||
|
||||
this.fbPositiveAction.setOnClickListener {
|
||||
@@ -356,7 +356,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setNegativeActionText(text: String?) {
|
||||
fun setNegativeActionText(text: String?) {
|
||||
if (TextUtils.isEmpty(text)) return
|
||||
|
||||
this.fbSecondaryActionContainer.visibility = VISIBLE
|
||||
@@ -364,7 +364,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.fbNegativeAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextSpanned(text: Spanned?) {
|
||||
fun setNegativeActionTextSpanned(text: Spanned?) {
|
||||
if (text == null) return
|
||||
|
||||
this.fbSecondaryActionContainer.visibility = VISIBLE
|
||||
@@ -372,32 +372,32 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.fbNegativeAction.visibility = VISIBLE
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextTypeface(typeface: Typeface?) {
|
||||
fun setNegativeActionTextTypeface(typeface: Typeface?) {
|
||||
if (typeface == null) return
|
||||
this.fbNegativeAction.typeface = typeface
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextSizeInPx(size: Float?) {
|
||||
fun setNegativeActionTextSizeInPx(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextSizeInSp(size: Float?) {
|
||||
fun setNegativeActionTextSizeInSp(size: Float?) {
|
||||
if (size == null) return
|
||||
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextColor(color: Int?) {
|
||||
fun setNegativeActionTextColor(color: Int?) {
|
||||
if (color == null) return
|
||||
this.fbNegativeAction.setTextColor(color)
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTextAppearance(messageAppearance: Int?) {
|
||||
fun setNegativeActionTextAppearance(messageAppearance: Int?) {
|
||||
if (messageAppearance == null) return
|
||||
this.fbNegativeAction.setTextAppearance(messageAppearance)
|
||||
}
|
||||
|
||||
internal fun setNegativeActionTapListener(listener: OnActionTapListener?) {
|
||||
fun setNegativeActionTapListener(listener: OnActionTapListener?) {
|
||||
if (listener == null) return
|
||||
|
||||
this.fbNegativeAction.setOnClickListener {
|
||||
@@ -415,17 +415,17 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
this.fbIcon.scaleType = scaleType
|
||||
}
|
||||
|
||||
internal fun setIconDrawable(icon: Drawable?) {
|
||||
fun setIconDrawable(icon: Drawable?) {
|
||||
if (icon == null) return
|
||||
this.fbIcon.setImageDrawable(icon)
|
||||
}
|
||||
|
||||
internal fun setIconBitmap(bitmap: Bitmap?) {
|
||||
fun setIconBitmap(bitmap: Bitmap?) {
|
||||
if (bitmap == null) return
|
||||
this.fbIcon.setImageBitmap(bitmap)
|
||||
}
|
||||
|
||||
internal fun setIconColorFilter(colorFilter: Int?, filterMode: PorterDuff.Mode?) {
|
||||
fun setIconColorFilter(colorFilter: Int?, filterMode: PorterDuff.Mode?) {
|
||||
if (colorFilter == null) return
|
||||
if (filterMode == null) {
|
||||
this.fbIcon.setColorFilter(colorFilter)
|
||||
@@ -449,7 +449,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setProgressPosition(position: ProgressPosition?) {
|
||||
fun setProgressPosition(position: ProgressPosition?) {
|
||||
if (position == null) return
|
||||
when (position) {
|
||||
LEFT -> {
|
||||
@@ -463,7 +463,7 @@ internal class FlashbarView(context: Context) : LinearLayout(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setProgressTint(progressTint: Int?, position: ProgressPosition?) {
|
||||
fun setProgressTint(progressTint: Int?, position: ProgressPosition?) {
|
||||
if (position == null || progressTint == null) return
|
||||
|
||||
val progressBar =
|
||||
|
||||
Reference in New Issue
Block a user