diff --git a/app/src/main/java/com/itsaky/androidide/actions/BaseBuildAction.kt b/app/src/main/java/com/itsaky/androidide/actions/BaseBuildAction.kt index ef5eca20b..d02626a30 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/BaseBuildAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/BaseBuildAction.kt @@ -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) { diff --git a/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt b/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt index 64fabb1a5..70c29eb7b 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/build/ProjectSyncAction.kt @@ -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() + } } diff --git a/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt b/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt index 2780a7bcb..69f9b955f 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/etc/PreviewLayoutAction.kt @@ -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) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt index 60e66d3b0..70e35f302 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/SaveFileAction.kt @@ -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() diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index e905a20a3..3f7eae5e3 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -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? = 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 @@ -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() } } diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index 6c6817879..87393ad18 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -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() diff --git a/app/src/main/java/com/itsaky/androidide/interfaces/IEditorHandler.kt b/app/src/main/java/com/itsaky/androidide/interfaces/IEditorHandler.kt index a00f6e21c..fadb3c389 100644 --- a/app/src/main/java/com/itsaky/androidide/interfaces/IEditorHandler.kt +++ b/app/src/main/java/com/itsaky/androidide/interfaces/IEditorHandler.kt @@ -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) diff --git a/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java b/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java deleted file mode 100644 index 03dfafb4f..000000000 --- a/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.java +++ /dev/null @@ -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 . - */ -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! - * - *

Marks this editor as unmodified. Used only when the activity is being destroyed. - */ - public void markUnmodified() { - binding.editor.markUnmodified(); - } - - /** - * For internal use only! - * - *

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); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.kt b/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.kt new file mode 100644 index 000000000..68a2f789d --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/ui/editor/CodeEditorView.kt @@ -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 . + */ + +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 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_editor.xml b/app/src/main/res/layout/activity_editor.xml index bad782ac5..5982f50cc 100755 --- a/app/src/main/res/layout/activity_editor.xml +++ b/app/src/main/res/layout/activity_editor.xml @@ -28,7 +28,7 @@ android:elevation="10dp" /> diff --git a/common/src/main/java/com/itsaky/androidide/tasks/coroutineUtils.kt b/common/src/main/java/com/itsaky/androidide/tasks/coroutineUtils.kt index 01b20da55..0c75a9991 100644 --- a/common/src/main/java/com/itsaky/androidide/tasks/coroutineUtils.kt +++ b/common/src/main/java/com/itsaky/androidide/tasks/coroutineUtils.kt @@ -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. diff --git a/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt b/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt index 408b7ff20..97bc54018 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/FlashbarActivityUtils.kt @@ -104,10 +104,10 @@ fun Activity.flashInfo(@StringRes msg: Int) { } @JvmOverloads -fun Activity.flashProgress( +fun 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) diff --git a/common/src/main/java/com/itsaky/androidide/utils/FlashbarUtils.kt b/common/src/main/java/com/itsaky/androidide/utils/FlashbarUtils.kt index 0e468e4e5..0ac666eb6 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/FlashbarUtils.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/FlashbarUtils.kt @@ -60,8 +60,11 @@ fun flashInfo(@StringRes msg: Int) { } @JvmOverloads -fun flashProgress(configure: Consumer? = null, action: Consumer) { - withActivity { flashProgress(configure, action) } +fun flashProgress( + configure: (Flashbar.Builder.() -> Unit)? = null, + action: (Flashbar) -> R? +) : R? { + return withActivity { flashProgress(configure, action) } } private fun withActivity(action: Activity.() -> T?): T? { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt index 50ef8fff9..f31e5a98d 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -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() - } - } } \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt new file mode 100644 index 000000000..882ab0fa6 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt @@ -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 . + */ + +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 + } +} \ No newline at end of file diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/ContentLockAccessor.kt b/editor/src/main/java/io/github/rosemoe/sora/text/ContentLockAccessor.kt new file mode 100644 index 000000000..10b7e3325 --- /dev/null +++ b/editor/src/main/java/io/github/rosemoe/sora/text/ContentLockAccessor.kt @@ -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 . + */ + +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) + } +} \ No newline at end of file diff --git a/editor/src/main/res/layout/layout_code_editor.xml b/editor/src/main/res/layout/layout_code_editor.xml index 57100ee1d..4d09d196b 100644 --- a/editor/src/main/res/layout/layout_code_editor.xml +++ b/editor/src/main/res/layout/layout_code_editor.xml @@ -1,5 +1,4 @@ - - - - - - + android:indeterminate="false" + android:max="100" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:progress="50" /> - + - - - + diff --git a/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/Flashbar.kt b/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/Flashbar.kt index 45992baa5..e9f382f63 100644 --- a/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/Flashbar.kt +++ b/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/Flashbar.kt @@ -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() { diff --git a/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/FlashbarView.kt b/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/FlashbarView.kt index fc1fe86b3..7976ab171 100644 --- a/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/FlashbarView.kt +++ b/subprojects/flashbar/src/main/java/com/itsaky/androidide/flashbar/FlashbarView.kt @@ -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 =