diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/AbstractDependentVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/AbstractDependentVal.kt index 9cd36415..5123c6c8 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/AbstractDependentVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/AbstractDependentVal.kt @@ -11,7 +11,7 @@ import world.phantasmal.observable.Observer * disposables need to be managed when e.g. [map] is used. */ abstract class AbstractDependentVal( - private val dependencies: Iterable>, + private vararg val dependencies: Val<*>, ) : AbstractVal() { /** * Is either empty or has a disposable per dependency. diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt index 8d3a954a..6dd9e02e 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt @@ -4,8 +4,8 @@ package world.phantasmal.observable.value * Val of which the value depends on 0 or more other vals. */ class DependentVal( - dependencies: Iterable>, + vararg dependencies: Val<*>, private val compute: () -> T, -) : AbstractDependentVal(dependencies) { +) : AbstractDependentVal(*dependencies) { override fun computeValue(): T = compute() } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatteningDependentVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatteningDependentVal.kt index 53414430..5c87bd65 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatteningDependentVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatteningDependentVal.kt @@ -9,9 +9,9 @@ import world.phantasmal.observable.Observer * Similar to [DependentVal], except that this val's [compute] returns a val. */ class FlatteningDependentVal( - dependencies: Iterable>, + vararg dependencies: Val<*>, private val compute: () -> Val, -) : AbstractDependentVal(dependencies) { +) : AbstractDependentVal(*dependencies) { private var computedVal: Val? = null private var computedValObserver: Disposable? = null diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt index d9c3ed7b..bfbcec1e 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt @@ -3,8 +3,8 @@ package world.phantasmal.observable.value import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.Observable 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.ListVal import kotlin.reflect.KProperty /** @@ -26,10 +26,10 @@ interface Val : Observable { * @param transform called whenever this val changes */ fun map(transform: (T) -> R): Val = - DependentVal(listOf(this)) { transform(value) } + DependentVal(this) { transform(value) } fun mapToListVal(transform: (T) -> List): ListVal = - 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 @@ -38,10 +38,10 @@ interface Val : Observable { * @param transform called whenever this val changes */ fun flatMap(transform: (T) -> Val): Val = - FlatteningDependentVal(listOf(this)) { transform(value) } + FlatteningDependentVal(this) { transform(value) } fun flatMapNull(transform: (T) -> Val?): Val = - FlatteningDependentVal(listOf(this)) { transform(value) ?: nullVal() } + FlatteningDependentVal(this) { transform(value) ?: nullVal() } fun isNull(): Val = map { it == null } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValCreation.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValCreation.kt index f2c04a21..50ca9018 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValCreation.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValCreation.kt @@ -40,7 +40,7 @@ fun map( v2: Val, transform: (T1, T2) -> R, ): Val = - DependentVal(listOf(v1, v2)) { transform(v1.value, v2.value) } + DependentVal(v1, v2) { transform(v1.value, v2.value) } /** * Map a transformation function over 3 vals. @@ -53,7 +53,7 @@ fun map( v3: Val, transform: (T1, T2, T3) -> R, ): Val = - 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 @@ -66,4 +66,7 @@ fun flatMap( v2: Val, transform: (T1, T2) -> Val, ): Val = - FlatteningDependentVal(listOf(v1, v2)) { transform(v1.value, v2.value) } + FlatteningDependentVal(v1, v2) { transform(v1.value, v2.value) } + +fun and(vararg vals: Val): Val = + DependentVal(*vals) { vals.all { it.value } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractDependentListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractDependentListVal.kt index 23e74ff5..5e9fdc66 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractDependentListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractDependentListVal.kt @@ -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. */ abstract class AbstractDependentListVal( - private val dependencies: List>, + private vararg val dependencies: Val<*>, ) : AbstractListVal(extractObservables = null) { private val _sizeVal = SizeVal() diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt index 55672bd3..38d97493 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt @@ -67,7 +67,7 @@ abstract class AbstractListVal( } override fun firstOrNull(): Val = - DependentVal(listOf(this)) { value.firstOrNull() } + DependentVal(this) { value.firstOrNull() } /** * Does the following in the given order: diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt index 04058b39..8189aff7 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt @@ -7,9 +7,9 @@ import world.phantasmal.observable.value.Val * ListVal of which the value depends on 0 or more other vals. */ class DependentListVal( - dependencies: List>, + vararg dependencies: Val<*>, private val computeElements: () -> List, -) : AbstractDependentListVal(dependencies) { +) : AbstractDependentListVal(*dependencies) { private var _elements: List? = null override val elements: List get() = _elements.unsafeAssertNotNull() diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListVal.kt index 6dc0f148..de808b13 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListVal.kt @@ -8,9 +8,9 @@ import world.phantasmal.observable.value.Val * Similar to [DependentListVal], except that this val's [computeElements] returns a ListVal. */ class FlatteningDependentListVal( - dependencies: List>, + vararg dependencies: Val<*>, private val computeElements: () -> ListVal, -) : AbstractDependentListVal(dependencies) { +) : AbstractDependentListVal(*dependencies) { private var computedVal: ListVal? = null private var computedValObserver: Disposable? = null diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt index 5f2e4074..737202d1 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt @@ -23,6 +23,9 @@ interface ListVal : Val> { fun fold(initialValue: R, operation: (R, E) -> R): Val = FoldedVal(this, initialValue, operation) + fun all(predicate: (E) -> Boolean): Val = + fold(true) { acc, el -> acc && predicate(el) } + fun sumBy(selector: (E) -> Int): Val = fold(0) { acc, el -> acc + selector(el) } @@ -30,4 +33,6 @@ interface ListVal : Val> { FilteredListVal(this, predicate) fun firstOrNull(): Val + + operator fun contains(element: @UnsafeVariance E): Boolean = element in value } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListValCreation.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListValCreation.kt index 97f53113..8a1f1824 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListValCreation.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListValCreation.kt @@ -18,4 +18,4 @@ fun flatMapToList( v2: Val, transform: (T1, T2) -> ListVal, ): ListVal = - FlatteningDependentListVal(listOf(v1, v2)) { transform(v1.value, v2.value) } + FlatteningDependentListVal(v1, v2) { transform(v1.value, v2.value) } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/DependentValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/DependentValTests.kt index 677a5be8..7cf95d6f 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/DependentValTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/DependentValTests.kt @@ -4,7 +4,7 @@ class DependentValTests : RegularValTests { override fun createProvider() = object : ValTests.Provider { val v = SimpleVal(0) - override val observable = DependentVal(listOf(v)) { 2 * v.value } + override val observable = DependentVal(v) { 2 * v.value } override fun emit() { v.value += 2 @@ -13,6 +13,6 @@ class DependentValTests : RegularValTests { override fun createWithValue(value: T): DependentVal { val v = SimpleVal(value) - return DependentVal(listOf(v)) { v.value } + return DependentVal(v) { v.value } } } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValDependentValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValDependentValEmitsTests.kt index 4df4611d..6bb03b5a 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValDependentValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValDependentValEmitsTests.kt @@ -11,7 +11,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests { override fun createProvider() = object : ValTests.Provider { val v = SimpleVal(StaticVal(5)) - override val observable = FlatteningDependentVal(listOf(v)) { v.value } + override val observable = FlatteningDependentVal(v) { v.value } override fun emit() { v.value = StaticVal(v.value.value + 5) @@ -20,7 +20,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests { override fun createWithValue(value: T): FlatteningDependentVal { val v = StaticVal(StaticVal(value)) - return FlatteningDependentVal(listOf(v)) { v.value } + return FlatteningDependentVal(v) { v.value } } /** @@ -30,7 +30,7 @@ class FlatteningDependentValDependentValEmitsTests : RegularValTests { @Test fun emits_a_change_when_its_direct_val_dependency_changes() = test { val v = SimpleVal(SimpleVal(7)) - val fv = FlatteningDependentVal(listOf(v)) { v.value } + val fv = FlatteningDependentVal(v) { v.value } var observedValue: Int? = null disposer.add( diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValNestedValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValNestedValEmitsTests.kt index 878c83c3..69653188 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValNestedValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatteningDependentValNestedValEmitsTests.kt @@ -7,7 +7,7 @@ class FlatteningDependentValNestedValEmitsTests : RegularValTests { override fun createProvider() = object : ValTests.Provider { val v = StaticVal(SimpleVal(5)) - override val observable = FlatteningDependentVal(listOf(v)) { v.value } + override val observable = FlatteningDependentVal(v) { v.value } override fun emit() { v.value.value += 5 @@ -16,6 +16,6 @@ class FlatteningDependentValNestedValEmitsTests : RegularValTests { override fun createWithValue(value: T): FlatteningDependentVal { val v = StaticVal(StaticVal(value)) - return FlatteningDependentVal(listOf(v)) { v.value } + return FlatteningDependentVal(v) { v.value } } } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt index 728cd011..624f4ae0 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt @@ -4,7 +4,7 @@ class DependentListValTests : ListValTests { override fun createProvider() = object : ListValTests.Provider { private val l = SimpleListVal(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() { l.add(4) diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValDependentValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValDependentValEmitsTests.kt index 6040e860..c5acce9f 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValDependentValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValDependentValEmitsTests.kt @@ -14,7 +14,7 @@ class FlatteningDependentListValDependentValEmitsTests : ListValTests { private val dependencyVal = SimpleVal>(nestedVal) override val observable = - FlatteningDependentListVal(listOf(dependencyVal)) { dependencyVal.value } + FlatteningDependentListVal(dependencyVal) { dependencyVal.value } override fun addElement() { // Update the direct dependency. diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValNestedValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValNestedValEmitsTests.kt index 75b9d0f2..1edf17f1 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValNestedValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/FlatteningDependentListValNestedValEmitsTests.kt @@ -14,7 +14,7 @@ class FlatteningDependentListValNestedValEmitsTests : ListValTests { private val dependentVal = StaticVal>(nestedVal) override val observable = - FlatteningDependentListVal(listOf(dependentVal)) { dependentVal.value } + FlatteningDependentListVal(dependentVal) { dependentVal.value } override fun addElement() { // Update the nested dependency. diff --git a/web/src/main/kotlin/world/phantasmal/web/core/undo/SimpleUndo.kt b/web/src/main/kotlin/world/phantasmal/web/core/undo/SimpleUndo.kt deleted file mode 100644 index acbfe60b..00000000 --- a/web/src/main/kotlin/world/phantasmal/web/core/undo/SimpleUndo.kt +++ /dev/null @@ -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 = mutableVal(false) - override val canRedo: MutableVal = mutableVal(false) - - override val firstUndo: Val = canUndo.map { if (it) action else null } - override val firstRedo: Val = 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 - } -} diff --git a/web/src/main/kotlin/world/phantasmal/web/core/undo/Undo.kt b/web/src/main/kotlin/world/phantasmal/web/core/undo/Undo.kt index 166f35da..378b4684 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/undo/Undo.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/undo/Undo.kt @@ -17,7 +17,21 @@ interface Undo { */ val firstRedo: Val + /** + * 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 + fun undo(): 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() } diff --git a/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoManager.kt b/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoManager.kt index a69ae10e..f40b0818 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoManager.kt @@ -1,13 +1,11 @@ package world.phantasmal.web.core.undo -import world.phantasmal.observable.value.Val -import world.phantasmal.observable.value.falseVal -import world.phantasmal.observable.value.mutableVal -import world.phantasmal.observable.value.nullVal +import world.phantasmal.observable.value.* +import world.phantasmal.observable.value.list.mutableListVal import world.phantasmal.web.core.actions.Action class UndoManager { - private val undos = mutableListOf(NopUndo) + private val undos = mutableListVal(NopUndo) { arrayOf(it.atSavePoint) } private val _current = mutableVal(NopUndo) val current: Val = _current @@ -17,6 +15,12 @@ class UndoManager { val firstUndo: Val = current.flatMap { it.firstUndo } val firstRedo: Val = 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 = undos.all { it.atSavePoint.value } + fun addUndo(undo: Undo) { undos.add(undo) } @@ -37,26 +41,35 @@ class UndoManager { fun redo(): Boolean = current.value.redo() + /** + * Sets a save point on all undos. + */ + fun savePoint() { + undos.value.forEach { it.savePoint() } + } + /** * Resets all managed undos. */ fun reset() { - undos.forEach { it.reset() } + undos.value.forEach { it.reset() } } - fun anyCanUndo(): Boolean = - undos.any { it.canUndo.value } - private object NopUndo : Undo { override val canUndo = falseVal() override val canRedo = falseVal() override val firstUndo = nullVal() override val firstRedo = nullVal() + override val atSavePoint = trueVal() override fun undo(): Boolean = false override fun redo(): Boolean = false + override fun savePoint() { + // Do nothing. + } + override fun reset() { // Do nothing. } diff --git a/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoStack.kt b/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoStack.kt index 1911e16f..0e1f1dd2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoStack.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/undo/UndoStack.kt @@ -1,10 +1,7 @@ package world.phantasmal.web.core.undo -import world.phantasmal.observable.value.Val -import world.phantasmal.observable.value.gt +import world.phantasmal.observable.value.* 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 /** @@ -18,12 +15,9 @@ class UndoStack(manager: UndoManager) : Undo { * action that will be redone when calling [redo]. */ private val index = mutableVal(0) + private val savePointIndex = mutableVal(0) private var undoingOrRedoing = false - init { - manager.addUndo(this) - } - override val canUndo: Val = index gt 0 override val canRedo: Val = map(stack, index) { stack, index -> index < stack.size } @@ -32,6 +26,12 @@ class UndoStack(manager: UndoManager) : Undo { override val firstRedo: Val = index.map { stack.value.getOrNull(it) } + override val atSavePoint: Val = index eq savePointIndex + + init { + manager.addUndo(this) + } + fun push(action: Action): Action { if (!undoingOrRedoing) { 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() { stack.clear() index.value = 0 + savePointIndex.value = 0 } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt index 3bb5fbab..fece4e49 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -79,11 +79,11 @@ class QuestEditor( val entityImageRenderer = addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer)) - // When the user tries to leave and there's something on any of the undo stacks, ask whether - // the user really wants to leave. + // When the user tries to leave and there are unsaved changes, ask whether the user really + // wants to leave. addDisposable( window.disposableListener("beforeunload", { e: BeforeUnloadEvent -> - if (undoManager.anyCanUndo()) { + if (!undoManager.allAtSavePoint.value) { e.preventDefault() e.returnValue = "false" } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt index fd5719af..673af505 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt @@ -64,7 +64,11 @@ class QuestEditorToolbarController( // Saving val saveEnabled: Val = - savingEnabled and files.notEmpty and BrowserFeatures.fileSystemApi + and( + savingEnabled, + questEditorStore.canSaveChanges, + files.notEmpty + ) and BrowserFeatures.fileSystemApi val saveTooltip: Val = value( if (BrowserFeatures.fileSystemApi) "Save changes (Ctrl-S)" else "This browser doesn't support saving to an existing file" @@ -235,6 +239,8 @@ class QuestEditorToolbarController( binFile.writeBuffer(bin) datFile.writeBuffer(dat) } + + questEditorStore.questSaved() } catch (e: Throwable) { setResult( PwResult.build(logger) @@ -297,6 +303,8 @@ class QuestEditorToolbarController( URL.revokeObjectURL(url) document.body?.removeChild(a) } + + questEditorStore.questSaved() } catch (e: Throwable) { setResult( PwResult.build(logger) diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AsmStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AsmStore.kt index 75cd4a0b..4f1f26d9 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AsmStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/AsmStore.kt @@ -6,18 +6,16 @@ import world.phantasmal.core.disposable.Disposer import world.phantasmal.core.disposable.disposable import world.phantasmal.lib.asm.assemble import world.phantasmal.lib.asm.disassemble -import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.Observable -import world.phantasmal.observable.emitter import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.list.ListVal import world.phantasmal.observable.value.mutableVal -import world.phantasmal.web.core.undo.SimpleUndo import world.phantasmal.web.core.undo.UndoManager import world.phantasmal.web.externals.monacoEditor.* import world.phantasmal.web.questEditor.asm.AsmAnalyser import world.phantasmal.web.questEditor.asm.monaco.* 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.AsmRange import world.phantasmal.web.shared.messages.AssemblyProblem @@ -42,14 +40,7 @@ class AsmStore( */ private val modelDisposer = addDisposable(Disposer()) - private val _didUndo = emitter() - private val _didRedo = emitter() - private val undo = SimpleUndo( - undoManager, - "Script edits", - { _didUndo.emit(ChangeEvent(Unit)) }, - { _didRedo.emit(ChangeEvent(Unit)) }, - ) + private val undo = addDisposable(TextModelUndo(undoManager, "Script edits", _textModel)) val inlineStackArgs: Val = _inlineStackArgs @@ -57,8 +48,8 @@ class AsmStore( val editingEnabled: Val = questEditorStore.questEditingEnabled - val didUndo: Observable = _didUndo - val didRedo: Observable = _didRedo + val didUndo: Observable = undo.didUndo + val didRedo: Observable = undo.didRedo val problems: ListVal = asmAnalyser.problems @@ -134,8 +125,6 @@ class AsmStore( _textModel.value = createModel(asm.joinToString("\n"), ASM_LANG_ID).also { model -> modelDisposer.add(disposable { model.dispose() }) - setupUndoRedo(model) - model.onDidChangeContent { e -> asmAnalyser.updateAsm(e.changes.map { AsmChange( @@ -168,42 +157,6 @@ class AsmStore( ?.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 { private val asmAnalyser = AsmAnalyser() diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt index da3211cf..b770b312 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt @@ -58,6 +58,7 @@ class QuestEditorStore( val firstUndo: Val = undoManager.firstUndo val canRedo: Val = questEditingEnabled and undoManager.canRedo val firstRedo: Val = undoManager.firstRedo + val canSaveChanges: Val = !undoManager.allAtSavePoint val showCollisionGeometry: Val = _showCollisionGeometry @@ -233,4 +234,8 @@ class QuestEditorStore( fun setShowCollisionGeometry(show: Boolean) { _showCollisionGeometry.value = show } + + fun questSaved() { + undoManager.savePoint() + } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/undo/TextModelUndo.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/undo/TextModelUndo.kt new file mode 100644 index 00000000..01df2a9b --- /dev/null +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/undo/TextModelUndo.kt @@ -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, +) : 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 = mutableVal(false) + private val _canRedo: MutableVal = mutableVal(false) + private val _didUndo = emitter() + private val _didRedo = emitter() + + private val currentVersionId = mutableVal(null) + private val savePointVersionId = mutableVal(null) + + override val canUndo: Val = _canUndo + override val canRedo: Val = _canRedo + + override val firstUndo: Val = canUndo.map { if (it) action else null } + override val firstRedo: Val = canRedo.map { if (it) action else null } + + override val atSavePoint: Val = savePointVersionId eq currentVersionId + + val didUndo: Observable = _didUndo + val didRedo: Observable = _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 + } +}