fix: crash when undoing attribute addition

This commit is contained in:
Akash Yadav
2022-12-28 22:15:22 +05:30
parent c7791b2506
commit 70f48d4c20
18 changed files with 154 additions and 81 deletions
@@ -30,6 +30,7 @@ import com.blankj.utilcode.util.FileUtils;
import com.itsaky.androidide.activities.editor.EditorHandlerActivity;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.editor.ui.IDEEditor;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.lsp.api.ILanguageClient;
import com.itsaky.androidide.lsp.models.CodeActionItem;
@@ -44,10 +45,9 @@ import com.itsaky.androidide.models.Location;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.ui.editor.CodeEditorView;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.ui.editor.CodeEditorView;
import com.itsaky.androidide.editor.ui.IDEEditor;
import com.itsaky.toaster.ToastUtilsKt;
import com.itsaky.toaster.Toaster;
@@ -89,7 +89,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
if (mInstance != null) {
throw new IllegalStateException("Client is already initialized");
}
mInstance = new IDELanguageClientImpl(provider);
return getInstance();
@@ -110,7 +110,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
public static boolean isInitialized() {
return mInstance != null;
}
@Override
public void publishDiagnostics(DiagnosticResult result) {
if (result == DiagnosticResult.NO_UPDATE) {
@@ -269,8 +269,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
}
@Override
public ShowDocumentResult showDocument(
ShowDocumentParams params) {
public ShowDocumentResult showDocument(ShowDocumentParams params) {
boolean success = false;
final var result = new ShowDocumentResult(false);
if (activity() == null) {
@@ -441,7 +440,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
private CodeEditorView findEditorByFile(File file) {
return activity().getEditorForFile(file);
}
private Unit noOp(final Object obj) {
return Unit.INSTANCE;
}
@@ -251,7 +251,7 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito
*/
@Override
public void setSelection(@NonNull Range range) {
if (isValidRange(range, true)) {
if (isValidPosition(range.getStart(), true) && isValidPosition(range.getEnd(), true)) {
setSelectionRegion(
range.getStart().getLine(),
range.getStart().getColumn(),
@@ -290,7 +290,6 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito
* Validates the range if it is invalid and returns a valid range.
*
* @param range Th range to validate.
* @return A new, validated range.
*/
@Override
public void validateRange(@NonNull final Range range) {
Binary file not shown.
@@ -33,6 +33,7 @@ import com.itsaky.androidide.uidesigner.adapters.AddAttrListAdapter
import com.itsaky.androidide.uidesigner.adapters.ViewAttrListAdapter
import com.itsaky.androidide.uidesigner.databinding.LayoutAddAttrBinding
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoHeaderBinding
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
/**
@@ -88,10 +89,9 @@ class AddAttrFragment : Fragment() {
val attributes =
adapter.supportedAttributes.filterNot { view.hasAttribute(it.namespace.uri, it.name) }
binding.attrList.adapter = AddAttrListAdapter(attributes) {
val attribute = newAttribute(view = viewModel.view, namespace = it.namespace, name = it.name, value = it.value)
viewModel.selectedAttr = attribute
val attribute = newAttribute(view = viewModel.view, namespace = it.namespace, name = it.name, value = "")
viewModel.selectedAttr = attribute as UiAttribute
viewModel.addAttrMode = true
view.addAttribute(attribute)
findNavController().navigate(R.id.attrValueEditorFragment)
}
}
@@ -26,10 +26,11 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.utils.newAttribute
import com.itsaky.androidide.uidesigner.databinding.LayoutAttrValueEditorBinding
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoHeaderBinding
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.utils.ValueCompletionProvider
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
@@ -56,11 +57,6 @@ class AttrValueEditorFragment : Fragment() {
private var textWatcher: TextWatcher? = null
companion object {
const val KEY_EDIT_RESULT = "ide.uidesigner.viewinfo.attreditor.result"
const val KEY_EDIT_RESULT_VALUE = "ide.uidesigner.viewinfo.attreditor.result.value"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -80,6 +76,7 @@ class AttrValueEditorFragment : Fragment() {
private fun showAttrInfo() {
val binding = this.binding ?: return
val header = this.header ?: return
val view = this.viewModel.view ?: return
val attr = this.viewModel.selectedAttr ?: return
header.name.text = "${attr.namespace.prefix}:${attr.name}"
@@ -94,11 +91,10 @@ class AttrValueEditorFragment : Fragment() {
)
textView.dropDownVerticalOffset = -binding.root.height
textView.showDropDown()
parentFragmentManager.setFragmentResult(
KEY_EDIT_RESULT,
Bundle().apply { putString(KEY_EDIT_RESULT_VALUE, binding.attrValue.text.toString()) }
)
val value = binding.attrValue.text.toString()
attr.value = value
view.applyAttribute(attr.copyAttr(value = value))
}
textView.setText(attr.value)
@@ -32,12 +32,15 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.itsaky.androidide.uidesigner.R
import com.itsaky.androidide.uidesigner.databinding.LayoutViewInfoSheetBinding
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class ViewInfoSheet : BottomSheetDialogFragment() {
private var binding: LayoutViewInfoSheetBinding? = null
private val viewModel by viewModels<WorkspaceViewModel>(ownerProducer = { requireActivity() })
private val log = ILogger.newInstance("ViewInfoSheet")
companion object {
const val TAG = "ide.uidesigner.viewinfo"
}
@@ -70,16 +73,24 @@ class ViewInfoSheet : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findNavControllerFromFragment()?.addOnDestinationChangedListener { _, destination, _ ->
findNavControllerFromFragment().addOnDestinationChangedListener { _, destination, _ ->
viewInfoBackPressedCallback.isEnabled = destination.id != R.id.viewInfoFragment
if (destination.id != R.id.attrValueEditorFragment) {
// not disabling the undo manager when going into 'edit mode' will result in a lot
// AttrUpdatedAction being pushed to UndoManager's undo stack.
viewModel.undoManager.enabled = destination.id != R.id.attrValueEditorFragment
if (viewModel.currentDestination == R.id.attrValueEditorFragment) {
// if we are returning from value editor, then
// 1 - if we were updating the value of an existing attribute, then the updated value must
// be updated in the view
// 2 - if we were adding a new attribute, then that attribute should be added to the view.
viewModel.notifyAttrUpdated()
viewModel.selectedAttr = null
viewModel.addAttrMode = false
} else {
viewModel.undoManager.disable()
}
viewModel.currentDestination = destination.id
}
(requireDialog() as ComponentDialog)
@@ -56,7 +56,7 @@ internal class WorkspaceViewAttrHandler : IView.AttributeChangeListener {
frag.undoManager.push(
AttrUpdatedAction(
view = view,
attr = (attribute as UiAttribute).copyAttr(view = view) as UiAttribute,
attr = attribute as UiAttribute,
oldValue = oldValue
)
)
@@ -17,13 +17,19 @@
package com.itsaky.androidide.uidesigner.undo
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.utils.ILogger
/**
* [UndoManager] action related to view attributes.
*
* @author Akash Yadav
*/
internal abstract class AttrAction(protected val view: IView, protected val attr: UiAttribute) : IUiAction
internal abstract class AttrAction(protected val view: IView, protected val attr: UiAttribute) :
IUiAction {
override fun toString(): String {
return "${javaClass.simpleName}(view=$view, attr=$attr)"
}
}
@@ -25,7 +25,8 @@ import com.itsaky.androidide.uidesigner.models.UiAttribute
*
* @author Akash Yadav
*/
internal class AttrAddedAction(view: IView, attr: UiAttribute) : AttrAction(view, attr) {
internal class AttrAddedAction(view: IView, attr: UiAttribute) :
AttrAction(view, attr.copyAttr(view = view) as UiAttribute) {
override fun undo() {
view.removeAttribute(attr)
@@ -25,7 +25,8 @@ import com.itsaky.androidide.uidesigner.models.UiAttribute
*
* @author Akash Yadav
*/
internal class AttrRemovedAction(view: IView, attr: UiAttribute) : AttrAction(view, attr) {
internal class AttrRemovedAction(view: IView, attr: UiAttribute) :
AttrAction(view, attr.copyAttr(view = view) as UiAttribute) {
override fun undo() {
view.addAttribute(attr)
@@ -17,6 +17,8 @@
package com.itsaky.androidide.uidesigner.undo
import com.itsaky.androidide.utils.ILogger
/**
* Handles undo and redo actions in the UI designer.
*
@@ -32,11 +34,11 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
companion object {
const val DEFAULT_STACK_SIZE = 30
}
fun enable() {
this.enabled = true
}
fun disable() {
this.enabled = false
}
@@ -86,7 +88,7 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
if (!enabled) {
return
}
redoStack.clear()
undoStack.addLast(action)
trimStacks()
@@ -20,7 +20,6 @@ package com.itsaky.androidide.uidesigner.utils
import android.text.Editable
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.internal.AttributeImpl
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.lsp.util.setupLookupForCompletion
import com.itsaky.androidide.lsp.xml.models.XMLServerSettings
@@ -64,10 +63,6 @@ internal class ValueCompletionProvider(
this.prefix = value
this.start()
}
val copy = (attribute as AttributeImpl).copyAttr(view = view)
copy.value = value
view.updateAttribute(copy)
}
class CompletionThread(
@@ -19,11 +19,8 @@ package com.itsaky.androidide.uidesigner.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.models.UiAttribute
import com.itsaky.androidide.uidesigner.undo.AttrAddedAction
import com.itsaky.androidide.uidesigner.undo.AttrUpdatedAction
import com.itsaky.androidide.uidesigner.undo.UndoManager
import java.io.File
@@ -32,9 +29,10 @@ internal class WorkspaceViewModel : ViewModel() {
internal val _errText = MutableLiveData("")
internal val _workspaceScreen = MutableLiveData(SCREEN_WORKSPACE)
internal val _view = MutableLiveData<IView>(null)
internal val _selectedAttr = MutableLiveData<IAttribute>(null)
internal val _selectedAttr = MutableLiveData<UiAttribute>(null)
internal val _addAttrMode = MutableLiveData(false)
internal val _undoManager = MutableLiveData(UndoManager())
internal val _currentDestination = MutableLiveData(0)
private val _file = MutableLiveData<File>()
companion object {
@@ -76,7 +74,7 @@ internal class WorkspaceViewModel : ViewModel() {
this._view.value = value
}
var selectedAttr: IAttribute?
var selectedAttr: UiAttribute?
get() = this._selectedAttr.value
set(value) {
this._selectedAttr.value = value
@@ -88,31 +86,44 @@ internal class WorkspaceViewModel : ViewModel() {
this._addAttrMode.value = value
}
var currentDestination: Int
get() = this._currentDestination.value ?: 0
set(value) {
this._currentDestination.value = value
}
fun notifyAttrUpdated() {
val attr = this.selectedAttr ?: return
val view = this.view ?: return
val undoManager = this.undoManager
if (addAttrMode) {
if (attr.value.isBlank()) {
view.removeAttribute(attr)
return
}
undoManager.push(AttrAddedAction(view, attr as UiAttribute))
if (addAttrMode && attr.value.isNotBlank()) {
// if we were adding a new attribute, then add the attribute to the view.
view.addAttribute(attr)
addAttrMode = false
selectedAttr = null
return
}
val existing = view.findAttribute(attr.namespace.uri, attr.name)
if (existing !is UiAttribute || existing.value == attr.value) {
// value of the attribute is same as before
if (existing !is UiAttribute) {
return
}
undoManager.push(
AttrUpdatedAction(view, existing.copyAttr(view = view) as UiAttribute, attr.value)
)
if (existing.value.isBlank()) {
// the user left the value field blank, remove the attribute
view.removeAttribute(existing)
selectedAttr = null
return
}
if (existing.value == attr.value) {
// value of the attribute is same as before
selectedAttr = null
return
}
// the user was updating the value af the attribute, so update it in the view
view.updateAttribute(existing.copyAttr(view = view, value = attr.value))
this.selectedAttr = null
}
}
@@ -69,6 +69,14 @@ interface IView {
*/
fun updateAttribute(attribute: IAttribute)
/**
* Applies the given attribute without adding it to the attributes list or notifying the attribute
* change listeners. Not resetting the attribute to its original value may have unexpected result.
*
* @param attribute The attribute to apply.
*/
fun applyAttribute(attribute: IAttribute)
/**
* Checks whether this view has an attribute entry with the given [namespaceUri] and [name].
*
@@ -28,6 +28,7 @@ import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.R.drawable
import com.itsaky.androidide.inflater.internal.utils.simpleName
import com.itsaky.androidide.inflater.internal.utils.tagName
import com.itsaky.androidide.inflater.utils.newAttribute
import com.itsaky.androidide.utils.ILogger
open class ViewImpl
@@ -68,19 +69,22 @@ constructor(
}
override fun removeAttribute(attribute: IAttribute) {
val exising = findAttribute(attribute) ?: return
val value = exising.value
// reset the value of the attribute to its default value
attribute.value = ""
updateAttributeInternal(attribute = attribute, notify = false)
this._attributes.remove(attribute)
notifyAttrRemoved(attribute)
applyAttribute(attribute = newAttribute(view = this, attribute = exising, value = ""))
if (this._attributes.remove(exising)) {
notifyAttrRemoved(newAttribute(view = this, attribute = exising, value = value))
}
}
override fun updateAttribute(attribute: IAttribute) {
updateAttributeInternal(attribute)
}
override fun findAttribute(namespaceUri: String, name: String): IAttribute? {
return this.attributes.find { it.namespace.uri == namespaceUri && it.name == name }
}
@@ -94,7 +98,7 @@ constructor(
view.foreground = this.fg
}
}
override fun registerAttributeChangeListener(listener: AttributeChangeListener) {
if (this.attrChangeListeners.contains(listener)) {
log.warn("Attempt to register an already-registered AttributeChangeListener")
@@ -102,12 +106,12 @@ constructor(
}
this.attrChangeListeners.add(listener)
}
override fun unregisterAttributeChangeListener(listener: AttributeChangeListener) {
this.attrChangeListeners.remove(listener)
}
protected open fun applyAttribute(attribute: IAttribute) {
override fun applyAttribute(attribute: IAttribute) {
val adapter = ViewAdapterIndex.getAdapter(name)
if (adapter == null) {
log.warn("No attribute adapter found for view $name")
@@ -115,7 +119,7 @@ constructor(
}
adapter.apply(this, attribute)
}
fun findNamespaces(): Set<INamespace> {
return hashSetOf<INamespace>().apply {
addAll(namespaces.values)
@@ -124,12 +128,12 @@ constructor(
}
}
}
fun findNamespaceByUri(uri: String): INamespace? {
return this.namespaces[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri)
}
open fun immutable() : IView {
open fun immutable(): IView {
return ImmutableViewImpl(this)
}
@@ -150,33 +154,33 @@ constructor(
builder.append(name)
builder.append("\n")
}
private fun updateAttributeInternal(attribute: IAttribute, notify: Boolean = true) {
val existing =
findAttribute(attribute)
?: throw IllegalArgumentException("Attribute '${attribute.name}' not found")
val oldVal = existing.value
existing.value = attribute.value
applyAttribute(existing)
if (notify) {
notifyAttrUpdated(attribute, oldVal)
notifyAttrUpdated(newAttribute(view = this, attribute = attribute), oldVal)
}
}
private fun notifyAttrAdded(attribute: IAttribute) {
for (listener in this.attrChangeListeners) {
listener.onAttributeAdded(this, attribute)
}
}
private fun notifyAttrRemoved(attribute: IAttribute) {
for (listener in this.attrChangeListeners) {
listener.onAttributeRemoved(this, attribute)
}
}
private fun notifyAttrUpdated(attribute: IAttribute, oldValue: String) {
for (listener in this.attrChangeListeners) {
listener.onAttributeUpdated(this, attribute, oldValue)
@@ -52,7 +52,7 @@ import com.itsaky.androidide.inflater.utils.newAttribute
open class ViewAdapter<T : View> : IViewAdapter<T>() {
override fun createAttrHandlers(create: (String, AttributeHandlerScope<T>.() -> Unit) -> Unit) {
create("alpha") { view.alpha = parseFloat(value) }
create("alpha") { view.alpha = parseFloat(value, def = 1f) }
create("background") { view.background = parseDrawable(context, value) }
create("backgroundTint") { view.backgroundTintList = parseColorStateList(context, value) }
create("backgroundTintMode") { view.backgroundTintMode = parsePorterDuffMode(value) }
@@ -0,0 +1,24 @@
/*
* 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.inflater.internal.adapters.appcompat
/**
* @author Akash Yadav
*/
class AppCompatTextView {
}
@@ -36,6 +36,22 @@ fun lookupComponentFactory(): IComponentFactory? {
return Lookup.DEFAULT.lookup(IComponentFactory.LAYOUT_INFLATER_COMPONENT_FACTORY_KEY)
}
@JvmOverloads
fun newAttribute(
view: IView? = null,
attribute: IAttribute,
namespace: INamespace? = null,
name: String? = null,
value: String? = null
): IAttribute {
return newAttribute(
view = view,
namespace = namespace ?: attribute.namespace,
name = name ?: attribute.name,
value = value ?: attribute.value
)
}
@JvmOverloads
fun newAttribute(
view: IView? = null,