fix(editor): use coroutines for reading/writing files in CodeEditorView

This commit is contained in:
Akash Yadav
2023-10-04 18:08:21 +05:30
parent 1a79c0bbf2
commit 9db0b802cd
19 changed files with 1081 additions and 632 deletions
@@ -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()
}
}
@@ -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()
}
}
}
+1 -1
View File
@@ -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 =