The save button is now disabled when there are no changes to save. The beforeunload dialog is now only shown when there are unsaved changes.

This commit is contained in:
Daan Vanden Bosch 2021-04-16 15:36:42 +02:00
parent bc660b23e9
commit a823e96f68
26 changed files with 250 additions and 159 deletions

View File

@ -11,7 +11,7 @@ import world.phantasmal.observable.Observer
* disposables need to be managed when e.g. [map] is used. * disposables need to be managed when e.g. [map] is used.
*/ */
abstract class AbstractDependentVal<T>( abstract class AbstractDependentVal<T>(
private val dependencies: Iterable<Val<*>>, private vararg val dependencies: Val<*>,
) : AbstractVal<T>() { ) : AbstractVal<T>() {
/** /**
* Is either empty or has a disposable per dependency. * Is either empty or has a disposable per dependency.

View File

@ -4,8 +4,8 @@ package world.phantasmal.observable.value
* Val of which the value depends on 0 or more other vals. * Val of which the value depends on 0 or more other vals.
*/ */
class DependentVal<T>( class DependentVal<T>(
dependencies: Iterable<Val<*>>, vararg dependencies: Val<*>,
private val compute: () -> T, private val compute: () -> T,
) : AbstractDependentVal<T>(dependencies) { ) : AbstractDependentVal<T>(*dependencies) {
override fun computeValue(): T = compute() override fun computeValue(): T = compute()
} }

View File

@ -9,9 +9,9 @@ import world.phantasmal.observable.Observer
* Similar to [DependentVal], except that this val's [compute] returns a val. * Similar to [DependentVal], except that this val's [compute] returns a val.
*/ */
class FlatteningDependentVal<T>( class FlatteningDependentVal<T>(
dependencies: Iterable<Val<*>>, vararg dependencies: Val<*>,
private val compute: () -> Val<T>, private val compute: () -> Val<T>,
) : AbstractDependentVal<T>(dependencies) { ) : AbstractDependentVal<T>(*dependencies) {
private var computedVal: Val<T>? = null private var computedVal: Val<T>? = null
private var computedValObserver: Disposable? = null private var computedValObserver: Disposable? = null

View File

@ -3,8 +3,8 @@ package world.phantasmal.observable.value
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.DependentListVal import world.phantasmal.observable.value.list.DependentListVal
import world.phantasmal.observable.value.list.ListVal
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
/** /**
@ -26,10 +26,10 @@ interface Val<out T> : Observable<T> {
* @param transform called whenever this val changes * @param transform called whenever this val changes
*/ */
fun <R> map(transform: (T) -> R): Val<R> = fun <R> map(transform: (T) -> R): Val<R> =
DependentVal(listOf(this)) { transform(value) } DependentVal(this) { transform(value) }
fun <R> mapToListVal(transform: (T) -> List<R>): ListVal<R> = fun <R> mapToListVal(transform: (T) -> List<R>): ListVal<R> =
DependentListVal(listOf(this)) { transform(value) } DependentListVal(this) { transform(value) }
/** /**
* Map a transformation function that returns a val over this val. The resulting val will change * Map a transformation function that returns a val over this val. The resulting val will change
@ -38,10 +38,10 @@ interface Val<out T> : Observable<T> {
* @param transform called whenever this val changes * @param transform called whenever this val changes
*/ */
fun <R> flatMap(transform: (T) -> Val<R>): Val<R> = fun <R> flatMap(transform: (T) -> Val<R>): Val<R> =
FlatteningDependentVal(listOf(this)) { transform(value) } FlatteningDependentVal(this) { transform(value) }
fun <R> flatMapNull(transform: (T) -> Val<R>?): Val<R?> = fun <R> flatMapNull(transform: (T) -> Val<R>?): Val<R?> =
FlatteningDependentVal(listOf(this)) { transform(value) ?: nullVal() } FlatteningDependentVal(this) { transform(value) ?: nullVal() }
fun isNull(): Val<Boolean> = fun isNull(): Val<Boolean> =
map { it == null } map { it == null }

View File

@ -40,7 +40,7 @@ fun <T1, T2, R> map(
v2: Val<T2>, v2: Val<T2>,
transform: (T1, T2) -> R, transform: (T1, T2) -> R,
): Val<R> = ): Val<R> =
DependentVal(listOf(v1, v2)) { transform(v1.value, v2.value) } DependentVal(v1, v2) { transform(v1.value, v2.value) }
/** /**
* Map a transformation function over 3 vals. * Map a transformation function over 3 vals.
@ -53,7 +53,7 @@ fun <T1, T2, T3, R> map(
v3: Val<T3>, v3: Val<T3>,
transform: (T1, T2, T3) -> R, transform: (T1, T2, T3) -> R,
): Val<R> = ): Val<R> =
DependentVal(listOf(v1, v2, v3)) { transform(v1.value, v2.value, v3.value) } DependentVal(v1, v2, v3) { transform(v1.value, v2.value, v3.value) }
/** /**
* Map a transformation function that returns a val over 2 vals. The resulting val will change when * Map a transformation function that returns a val over 2 vals. The resulting val will change when
@ -66,4 +66,7 @@ fun <T1, T2, R> flatMap(
v2: Val<T2>, v2: Val<T2>,
transform: (T1, T2) -> Val<R>, transform: (T1, T2) -> Val<R>,
): Val<R> = ): Val<R> =
FlatteningDependentVal(listOf(v1, v2)) { transform(v1.value, v2.value) } FlatteningDependentVal(v1, v2) { transform(v1.value, v2.value) }
fun and(vararg vals: Val<Boolean>): Val<Boolean> =
DependentVal(*vals) { vals.all { it.value } }

View File

@ -12,7 +12,7 @@ import world.phantasmal.observable.value.Val
* This way no extra disposables need to be managed when e.g. [map] is used. * This way no extra disposables need to be managed when e.g. [map] is used.
*/ */
abstract class AbstractDependentListVal<E>( abstract class AbstractDependentListVal<E>(
private val dependencies: List<Val<*>>, private vararg val dependencies: Val<*>,
) : AbstractListVal<E>(extractObservables = null) { ) : AbstractListVal<E>(extractObservables = null) {
private val _sizeVal = SizeVal() private val _sizeVal = SizeVal()

View File

@ -67,7 +67,7 @@ abstract class AbstractListVal<E>(
} }
override fun firstOrNull(): Val<E?> = override fun firstOrNull(): Val<E?> =
DependentVal(listOf(this)) { value.firstOrNull() } DependentVal(this) { value.firstOrNull() }
/** /**
* Does the following in the given order: * Does the following in the given order:

View File

@ -7,9 +7,9 @@ import world.phantasmal.observable.value.Val
* ListVal of which the value depends on 0 or more other vals. * ListVal of which the value depends on 0 or more other vals.
*/ */
class DependentListVal<E>( class DependentListVal<E>(
dependencies: List<Val<*>>, vararg dependencies: Val<*>,
private val computeElements: () -> List<E>, private val computeElements: () -> List<E>,
) : AbstractDependentListVal<E>(dependencies) { ) : AbstractDependentListVal<E>(*dependencies) {
private var _elements: List<E>? = null private var _elements: List<E>? = null
override val elements: List<E> get() = _elements.unsafeAssertNotNull() override val elements: List<E> get() = _elements.unsafeAssertNotNull()

View File

@ -8,9 +8,9 @@ import world.phantasmal.observable.value.Val
* Similar to [DependentListVal], except that this val's [computeElements] returns a ListVal. * Similar to [DependentListVal], except that this val's [computeElements] returns a ListVal.
*/ */
class FlatteningDependentListVal<E>( class FlatteningDependentListVal<E>(
dependencies: List<Val<*>>, vararg dependencies: Val<*>,
private val computeElements: () -> ListVal<E>, private val computeElements: () -> ListVal<E>,
) : AbstractDependentListVal<E>(dependencies) { ) : AbstractDependentListVal<E>(*dependencies) {
private var computedVal: ListVal<E>? = null private var computedVal: ListVal<E>? = null
private var computedValObserver: Disposable? = null private var computedValObserver: Disposable? = null

View File

@ -23,6 +23,9 @@ interface ListVal<out E> : Val<List<E>> {
fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> = fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> =
FoldedVal(this, initialValue, operation) FoldedVal(this, initialValue, operation)
fun all(predicate: (E) -> Boolean): Val<Boolean> =
fold(true) { acc, el -> acc && predicate(el) }
fun sumBy(selector: (E) -> Int): Val<Int> = fun sumBy(selector: (E) -> Int): Val<Int> =
fold(0) { acc, el -> acc + selector(el) } fold(0) { acc, el -> acc + selector(el) }
@ -30,4 +33,6 @@ interface ListVal<out E> : Val<List<E>> {
FilteredListVal(this, predicate) FilteredListVal(this, predicate)
fun firstOrNull(): Val<E?> fun firstOrNull(): Val<E?>
operator fun contains(element: @UnsafeVariance E): Boolean = element in value
} }

View File

@ -18,4 +18,4 @@ fun <T1, T2, R> flatMapToList(
v2: Val<T2>, v2: Val<T2>,
transform: (T1, T2) -> ListVal<R>, transform: (T1, T2) -> ListVal<R>,
): ListVal<R> = ): ListVal<R> =
FlatteningDependentListVal(listOf(v1, v2)) { transform(v1.value, v2.value) } FlatteningDependentListVal(v1, v2) { transform(v1.value, v2.value) }

View File

@ -4,7 +4,7 @@ class DependentValTests : RegularValTests {
override fun createProvider() = object : ValTests.Provider { override fun createProvider() = object : ValTests.Provider {
val v = SimpleVal(0) val v = SimpleVal(0)
override val observable = DependentVal(listOf(v)) { 2 * v.value } override val observable = DependentVal(v) { 2 * v.value }
override fun emit() { override fun emit() {
v.value += 2 v.value += 2
@ -13,6 +13,6 @@ class DependentValTests : RegularValTests {
override fun <T> createWithValue(value: T): DependentVal<T> { override fun <T> createWithValue(value: T): DependentVal<T> {
val v = SimpleVal(value) val v = SimpleVal(value)
return DependentVal(listOf(v)) { v.value } return DependentVal(v) { v.value }
} }
} }

View File

@ -11,7 +11,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests {
override fun createProvider() = object : ValTests.Provider { override fun createProvider() = object : ValTests.Provider {
val v = SimpleVal(StaticVal(5)) val v = SimpleVal(StaticVal(5))
override val observable = FlatteningDependentVal(listOf(v)) { v.value } override val observable = FlatteningDependentVal(v) { v.value }
override fun emit() { override fun emit() {
v.value = StaticVal(v.value.value + 5) v.value = StaticVal(v.value.value + 5)
@ -20,7 +20,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests {
override fun <T> createWithValue(value: T): FlatteningDependentVal<T> { override fun <T> createWithValue(value: T): FlatteningDependentVal<T> {
val v = StaticVal(StaticVal(value)) val v = StaticVal(StaticVal(value))
return FlatteningDependentVal(listOf(v)) { v.value } return FlatteningDependentVal(v) { v.value }
} }
/** /**
@ -30,7 +30,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests {
@Test @Test
fun emits_a_change_when_its_direct_val_dependency_changes() = test { fun emits_a_change_when_its_direct_val_dependency_changes() = test {
val v = SimpleVal(SimpleVal(7)) val v = SimpleVal(SimpleVal(7))
val fv = FlatteningDependentVal(listOf(v)) { v.value } val fv = FlatteningDependentVal(v) { v.value }
var observedValue: Int? = null var observedValue: Int? = null
disposer.add( disposer.add(

View File

@ -7,7 +7,7 @@ class FlatteningDependentValNestedValEmitsTests : RegularValTests {
override fun createProvider() = object : ValTests.Provider { override fun createProvider() = object : ValTests.Provider {
val v = StaticVal(SimpleVal(5)) val v = StaticVal(SimpleVal(5))
override val observable = FlatteningDependentVal(listOf(v)) { v.value } override val observable = FlatteningDependentVal(v) { v.value }
override fun emit() { override fun emit() {
v.value.value += 5 v.value.value += 5
@ -16,6 +16,6 @@ class FlatteningDependentValNestedValEmitsTests : RegularValTests {
override fun <T> createWithValue(value: T): FlatteningDependentVal<T> { override fun <T> createWithValue(value: T): FlatteningDependentVal<T> {
val v = StaticVal(StaticVal(value)) val v = StaticVal(StaticVal(value))
return FlatteningDependentVal(listOf(v)) { v.value } return FlatteningDependentVal(v) { v.value }
} }
} }

View File

@ -4,7 +4,7 @@ class DependentListValTests : ListValTests {
override fun createProvider() = object : ListValTests.Provider { override fun createProvider() = object : ListValTests.Provider {
private val l = SimpleListVal<Int>(mutableListOf()) private val l = SimpleListVal<Int>(mutableListOf())
override val observable = DependentListVal(listOf(l)) { l.value.map { 2 * it } } override val observable = DependentListVal(l) { l.value.map { 2 * it } }
override fun addElement() { override fun addElement() {
l.add(4) l.add(4)

View File

@ -14,7 +14,7 @@ class FlatteningDependentListValDependentValEmitsTests : ListValTests {
private val dependencyVal = SimpleVal<ListVal<Int>>(nestedVal) private val dependencyVal = SimpleVal<ListVal<Int>>(nestedVal)
override val observable = override val observable =
FlatteningDependentListVal(listOf(dependencyVal)) { dependencyVal.value } FlatteningDependentListVal(dependencyVal) { dependencyVal.value }
override fun addElement() { override fun addElement() {
// Update the direct dependency. // Update the direct dependency.

View File

@ -14,7 +14,7 @@ class FlatteningDependentListValNestedValEmitsTests : ListValTests {
private val dependentVal = StaticVal<ListVal<Int>>(nestedVal) private val dependentVal = StaticVal<ListVal<Int>>(nestedVal)
override val observable = override val observable =
FlatteningDependentListVal(listOf(dependentVal)) { dependentVal.value } FlatteningDependentListVal(dependentVal) { dependentVal.value }
override fun addElement() { override fun addElement() {
// Update the nested dependency. // Update the nested dependency.

View File

@ -1,57 +0,0 @@
package world.phantasmal.web.core.undo
import world.phantasmal.observable.value.*
import world.phantasmal.web.core.actions.Action
/**
* Simply contains a single action. [canUndo] and [canRedo] must be managed manually.
*/
class SimpleUndo(
undoManager: UndoManager,
private val description: String,
undo: () -> Unit,
redo: () -> Unit,
) : Undo {
private val action = object : Action {
override val description: String = this@SimpleUndo.description
override fun execute() {
redo()
}
override fun undo() {
undo()
}
}
override val canUndo: MutableVal<Boolean> = mutableVal(false)
override val canRedo: MutableVal<Boolean> = mutableVal(false)
override val firstUndo: Val<Action?> = canUndo.map { if (it) action else null }
override val firstRedo: Val<Action?> = canRedo.map { if (it) action else null }
init {
undoManager.addUndo(this)
}
override fun undo(): Boolean =
if (canUndo.value) {
action.undo()
true
} else {
false
}
override fun redo(): Boolean =
if (canRedo.value) {
action.execute()
true
} else {
false
}
override fun reset() {
canUndo.value = false
canRedo.value = false
}
}

View File

@ -17,7 +17,21 @@ interface Undo {
*/ */
val firstRedo: Val<Action?> val firstRedo: Val<Action?>
/**
* True if this undo is at the point in time where the last save happened. See [savePoint].
* If false, it should be safe to leave the application because no changes have happened since
* the last save point (either because there were no changes or all changes have been undone).
*/
val atSavePoint: Val<Boolean>
fun undo(): Boolean fun undo(): Boolean
fun redo(): Boolean fun redo(): Boolean
/**
* Called when a save happens, the undo should remember this point in time and reflect whether
* it's currently at this point in [atSavePoint].
*/
fun savePoint()
fun reset() fun reset()
} }

View File

@ -1,13 +1,11 @@
package world.phantasmal.web.core.undo package world.phantasmal.web.core.undo
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.*
import world.phantasmal.observable.value.falseVal import world.phantasmal.observable.value.list.mutableListVal
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.observable.value.nullVal
import world.phantasmal.web.core.actions.Action import world.phantasmal.web.core.actions.Action
class UndoManager { class UndoManager {
private val undos = mutableListOf<Undo>(NopUndo) private val undos = mutableListVal<Undo>(NopUndo) { arrayOf(it.atSavePoint) }
private val _current = mutableVal<Undo>(NopUndo) private val _current = mutableVal<Undo>(NopUndo)
val current: Val<Undo> = _current val current: Val<Undo> = _current
@ -17,6 +15,12 @@ class UndoManager {
val firstUndo: Val<Action?> = current.flatMap { it.firstUndo } val firstUndo: Val<Action?> = current.flatMap { it.firstUndo }
val firstRedo: Val<Action?> = current.flatMap { it.firstRedo } val firstRedo: Val<Action?> = current.flatMap { it.firstRedo }
/**
* True if all undos are at the most recent save point. I.e., true if there are no changes to
* save.
*/
val allAtSavePoint: Val<Boolean> = undos.all { it.atSavePoint.value }
fun addUndo(undo: Undo) { fun addUndo(undo: Undo) {
undos.add(undo) undos.add(undo)
} }
@ -37,26 +41,35 @@ class UndoManager {
fun redo(): Boolean = fun redo(): Boolean =
current.value.redo() current.value.redo()
/**
* Sets a save point on all undos.
*/
fun savePoint() {
undos.value.forEach { it.savePoint() }
}
/** /**
* Resets all managed undos. * Resets all managed undos.
*/ */
fun reset() { fun reset() {
undos.forEach { it.reset() } undos.value.forEach { it.reset() }
} }
fun anyCanUndo(): Boolean =
undos.any { it.canUndo.value }
private object NopUndo : Undo { private object NopUndo : Undo {
override val canUndo = falseVal() override val canUndo = falseVal()
override val canRedo = falseVal() override val canRedo = falseVal()
override val firstUndo = nullVal() override val firstUndo = nullVal()
override val firstRedo = nullVal() override val firstRedo = nullVal()
override val atSavePoint = trueVal()
override fun undo(): Boolean = false override fun undo(): Boolean = false
override fun redo(): Boolean = false override fun redo(): Boolean = false
override fun savePoint() {
// Do nothing.
}
override fun reset() { override fun reset() {
// Do nothing. // Do nothing.
} }

View File

@ -1,10 +1,7 @@
package world.phantasmal.web.core.undo package world.phantasmal.web.core.undo
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.*
import world.phantasmal.observable.value.gt
import world.phantasmal.observable.value.list.mutableListVal import world.phantasmal.observable.value.list.mutableListVal
import world.phantasmal.observable.value.map
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.actions.Action import world.phantasmal.web.core.actions.Action
/** /**
@ -18,12 +15,9 @@ class UndoStack(manager: UndoManager) : Undo {
* action that will be redone when calling [redo]. * action that will be redone when calling [redo].
*/ */
private val index = mutableVal(0) private val index = mutableVal(0)
private val savePointIndex = mutableVal(0)
private var undoingOrRedoing = false private var undoingOrRedoing = false
init {
manager.addUndo(this)
}
override val canUndo: Val<Boolean> = index gt 0 override val canUndo: Val<Boolean> = index gt 0
override val canRedo: Val<Boolean> = map(stack, index) { stack, index -> index < stack.size } override val canRedo: Val<Boolean> = map(stack, index) { stack, index -> index < stack.size }
@ -32,6 +26,12 @@ class UndoStack(manager: UndoManager) : Undo {
override val firstRedo: Val<Action?> = index.map { stack.value.getOrNull(it) } override val firstRedo: Val<Action?> = index.map { stack.value.getOrNull(it) }
override val atSavePoint: Val<Boolean> = index eq savePointIndex
init {
manager.addUndo(this)
}
fun push(action: Action): Action { fun push(action: Action): Action {
if (!undoingOrRedoing) { if (!undoingOrRedoing) {
stack.splice(index.value, stack.value.size - index.value, action) stack.splice(index.value, stack.value.size - index.value, action)
@ -67,8 +67,13 @@ class UndoStack(manager: UndoManager) : Undo {
} }
} }
override fun savePoint() {
savePointIndex.value = index.value
}
override fun reset() { override fun reset() {
stack.clear() stack.clear()
index.value = 0 index.value = 0
savePointIndex.value = 0
} }
} }

View File

@ -79,11 +79,11 @@ class QuestEditor(
val entityImageRenderer = val entityImageRenderer =
addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer)) addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer))
// When the user tries to leave and there's something on any of the undo stacks, ask whether // When the user tries to leave and there are unsaved changes, ask whether the user really
// the user really wants to leave. // wants to leave.
addDisposable( addDisposable(
window.disposableListener("beforeunload", { e: BeforeUnloadEvent -> window.disposableListener("beforeunload", { e: BeforeUnloadEvent ->
if (undoManager.anyCanUndo()) { if (!undoManager.allAtSavePoint.value) {
e.preventDefault() e.preventDefault()
e.returnValue = "false" e.returnValue = "false"
} }

View File

@ -64,7 +64,11 @@ class QuestEditorToolbarController(
// Saving // Saving
val saveEnabled: Val<Boolean> = val saveEnabled: Val<Boolean> =
savingEnabled and files.notEmpty and BrowserFeatures.fileSystemApi and(
savingEnabled,
questEditorStore.canSaveChanges,
files.notEmpty
) and BrowserFeatures.fileSystemApi
val saveTooltip: Val<String> = value( val saveTooltip: Val<String> = value(
if (BrowserFeatures.fileSystemApi) "Save changes (Ctrl-S)" if (BrowserFeatures.fileSystemApi) "Save changes (Ctrl-S)"
else "This browser doesn't support saving to an existing file" else "This browser doesn't support saving to an existing file"
@ -235,6 +239,8 @@ class QuestEditorToolbarController(
binFile.writeBuffer(bin) binFile.writeBuffer(bin)
datFile.writeBuffer(dat) datFile.writeBuffer(dat)
} }
questEditorStore.questSaved()
} catch (e: Throwable) { } catch (e: Throwable) {
setResult( setResult(
PwResult.build<Nothing>(logger) PwResult.build<Nothing>(logger)
@ -297,6 +303,8 @@ class QuestEditorToolbarController(
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
document.body?.removeChild(a) document.body?.removeChild(a)
} }
questEditorStore.questSaved()
} catch (e: Throwable) { } catch (e: Throwable) {
setResult( setResult(
PwResult.build<Nothing>(logger) PwResult.build<Nothing>(logger)

View File

@ -6,18 +6,16 @@ import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.disposable import world.phantasmal.core.disposable.disposable
import world.phantasmal.lib.asm.assemble import world.phantasmal.lib.asm.assemble
import world.phantasmal.lib.asm.disassemble import world.phantasmal.lib.asm.disassemble
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable import world.phantasmal.observable.Observable
import world.phantasmal.observable.emitter
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.mutableVal import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.undo.SimpleUndo
import world.phantasmal.web.core.undo.UndoManager import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.monacoEditor.* import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.web.questEditor.asm.AsmAnalyser import world.phantasmal.web.questEditor.asm.AsmAnalyser
import world.phantasmal.web.questEditor.asm.monaco.* import world.phantasmal.web.questEditor.asm.monaco.*
import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.undo.TextModelUndo
import world.phantasmal.web.shared.messages.AsmChange import world.phantasmal.web.shared.messages.AsmChange
import world.phantasmal.web.shared.messages.AsmRange import world.phantasmal.web.shared.messages.AsmRange
import world.phantasmal.web.shared.messages.AssemblyProblem import world.phantasmal.web.shared.messages.AssemblyProblem
@ -42,14 +40,7 @@ class AsmStore(
*/ */
private val modelDisposer = addDisposable(Disposer()) private val modelDisposer = addDisposable(Disposer())
private val _didUndo = emitter<Unit>() private val undo = addDisposable(TextModelUndo(undoManager, "Script edits", _textModel))
private val _didRedo = emitter<Unit>()
private val undo = SimpleUndo(
undoManager,
"Script edits",
{ _didUndo.emit(ChangeEvent(Unit)) },
{ _didRedo.emit(ChangeEvent(Unit)) },
)
val inlineStackArgs: Val<Boolean> = _inlineStackArgs val inlineStackArgs: Val<Boolean> = _inlineStackArgs
@ -57,8 +48,8 @@ class AsmStore(
val editingEnabled: Val<Boolean> = questEditorStore.questEditingEnabled val editingEnabled: Val<Boolean> = questEditorStore.questEditingEnabled
val didUndo: Observable<Unit> = _didUndo val didUndo: Observable<Unit> = undo.didUndo
val didRedo: Observable<Unit> = _didRedo val didRedo: Observable<Unit> = undo.didRedo
val problems: ListVal<AssemblyProblem> = asmAnalyser.problems val problems: ListVal<AssemblyProblem> = asmAnalyser.problems
@ -134,8 +125,6 @@ class AsmStore(
_textModel.value = createModel(asm.joinToString("\n"), ASM_LANG_ID).also { model -> _textModel.value = createModel(asm.joinToString("\n"), ASM_LANG_ID).also { model ->
modelDisposer.add(disposable { model.dispose() }) modelDisposer.add(disposable { model.dispose() })
setupUndoRedo(model)
model.onDidChangeContent { e -> model.onDidChangeContent { e ->
asmAnalyser.updateAsm(e.changes.map { asmAnalyser.updateAsm(e.changes.map {
AsmChange( AsmChange(
@ -168,42 +157,6 @@ class AsmStore(
?.let(quest::setBytecodeIr) ?.let(quest::setBytecodeIr)
} }
private fun setupUndoRedo(model: ITextModel) {
val initialVersion = model.getAlternativeVersionId()
var currentVersion = initialVersion
var lastVersion = initialVersion
model.onDidChangeContent {
val version = model.getAlternativeVersionId()
if (version < currentVersion) {
// Undoing.
undo.canRedo.value = true
if (version == initialVersion) {
undo.canUndo.value = false
}
} else {
// Redoing.
if (version <= lastVersion) {
if (version == lastVersion) {
undo.canRedo.value = false
}
} else {
undo.canRedo.value = false
if (currentVersion > lastVersion) {
lastVersion = currentVersion
}
}
undo.canUndo.value = true
}
currentVersion = version
}
}
companion object { companion object {
private val asmAnalyser = AsmAnalyser() private val asmAnalyser = AsmAnalyser()

View File

@ -58,6 +58,7 @@ class QuestEditorStore(
val firstUndo: Val<Action?> = undoManager.firstUndo val firstUndo: Val<Action?> = undoManager.firstUndo
val canRedo: Val<Boolean> = questEditingEnabled and undoManager.canRedo val canRedo: Val<Boolean> = questEditingEnabled and undoManager.canRedo
val firstRedo: Val<Action?> = undoManager.firstRedo val firstRedo: Val<Action?> = undoManager.firstRedo
val canSaveChanges: Val<Boolean> = !undoManager.allAtSavePoint
val showCollisionGeometry: Val<Boolean> = _showCollisionGeometry val showCollisionGeometry: Val<Boolean> = _showCollisionGeometry
@ -233,4 +234,8 @@ class QuestEditorStore(
fun setShowCollisionGeometry(show: Boolean) { fun setShowCollisionGeometry(show: Boolean) {
_showCollisionGeometry.value = show _showCollisionGeometry.value = show
} }
fun questSaved() {
undoManager.savePoint()
}
} }

View File

@ -0,0 +1,142 @@
package world.phantasmal.web.questEditor.undo
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable
import world.phantasmal.observable.emitter
import world.phantasmal.observable.value.MutableVal
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.eq
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.web.core.actions.Action
import world.phantasmal.web.core.undo.Undo
import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.monacoEditor.IDisposable
import world.phantasmal.web.externals.monacoEditor.ITextModel
class TextModelUndo(
undoManager: UndoManager,
private val description: String,
model: Val<ITextModel?>,
) : Undo, TrackedDisposable() {
private val action = object : Action {
override val description: String = this@TextModelUndo.description
override fun execute() {
_didRedo.emit(ChangeEvent(Unit))
}
override fun undo() {
_didUndo.emit(ChangeEvent(Unit))
}
}
private val modelObserver: Disposable
private var modelChangeObserver: IDisposable? = null
private val _canUndo: MutableVal<Boolean> = mutableVal(false)
private val _canRedo: MutableVal<Boolean> = mutableVal(false)
private val _didUndo = emitter<Unit>()
private val _didRedo = emitter<Unit>()
private val currentVersionId = mutableVal<Int?>(null)
private val savePointVersionId = mutableVal<Int?>(null)
override val canUndo: Val<Boolean> = _canUndo
override val canRedo: Val<Boolean> = _canRedo
override val firstUndo: Val<Action?> = canUndo.map { if (it) action else null }
override val firstRedo: Val<Action?> = canRedo.map { if (it) action else null }
override val atSavePoint: Val<Boolean> = savePointVersionId eq currentVersionId
val didUndo: Observable<Unit> = _didUndo
val didRedo: Observable<Unit> = _didRedo
init {
undoManager.addUndo(this)
modelObserver = model.observe(callNow = true) { onModelChange(it.value) }
}
override fun dispose() {
modelChangeObserver?.dispose()
modelObserver.dispose()
super.dispose()
}
private fun onModelChange(model: ITextModel?) {
modelChangeObserver?.dispose()
if (model == null) {
reset()
return
}
_canUndo.value = false
_canRedo.value = false
val initialVersionId = model.getAlternativeVersionId()
currentVersionId.value = initialVersionId
savePointVersionId.value = initialVersionId
var lastVersionId = initialVersionId
modelChangeObserver = model.onDidChangeContent {
val versionId = model.getAlternativeVersionId()
val prevVersionId = currentVersionId.value!!
if (versionId < prevVersionId) {
// Undoing.
_canRedo.value = true
if (versionId == initialVersionId) {
_canUndo.value = false
}
} else {
// Redoing.
if (versionId <= lastVersionId) {
if (versionId == lastVersionId) {
_canRedo.value = false
}
} else {
_canRedo.value = false
if (prevVersionId > lastVersionId) {
lastVersionId = prevVersionId
}
}
_canUndo.value = true
}
currentVersionId.value = versionId
}
}
override fun undo(): Boolean =
if (canUndo.value) {
action.undo()
true
} else {
false
}
override fun redo(): Boolean =
if (canRedo.value) {
action.execute()
true
} else {
false
}
override fun savePoint() {
savePointVersionId.value = currentVersionId.value
}
override fun reset() {
_canUndo.value = false
_canRedo.value = false
currentVersionId.value = null
savePointVersionId.value = null
}
}