mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
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:
parent
bc660b23e9
commit
a823e96f68
@ -11,7 +11,7 @@ import world.phantasmal.observable.Observer
|
||||
* disposables need to be managed when e.g. [map] is used.
|
||||
*/
|
||||
abstract class AbstractDependentVal<T>(
|
||||
private val dependencies: Iterable<Val<*>>,
|
||||
private vararg val dependencies: Val<*>,
|
||||
) : AbstractVal<T>() {
|
||||
/**
|
||||
* Is either empty or has a disposable per dependency.
|
||||
|
@ -4,8 +4,8 @@ package world.phantasmal.observable.value
|
||||
* Val of which the value depends on 0 or more other vals.
|
||||
*/
|
||||
class DependentVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
vararg dependencies: Val<*>,
|
||||
private val compute: () -> T,
|
||||
) : AbstractDependentVal<T>(dependencies) {
|
||||
) : AbstractDependentVal<T>(*dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
}
|
||||
|
@ -9,9 +9,9 @@ import world.phantasmal.observable.Observer
|
||||
* Similar to [DependentVal], except that this val's [compute] returns a val.
|
||||
*/
|
||||
class FlatteningDependentVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
vararg dependencies: Val<*>,
|
||||
private val compute: () -> Val<T>,
|
||||
) : AbstractDependentVal<T>(dependencies) {
|
||||
) : AbstractDependentVal<T>(*dependencies) {
|
||||
private var computedVal: Val<T>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
|
||||
|
@ -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<out T> : Observable<T> {
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
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> =
|
||||
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<out T> : Observable<T> {
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
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?> =
|
||||
FlatteningDependentVal(listOf(this)) { transform(value) ?: nullVal() }
|
||||
FlatteningDependentVal(this) { transform(value) ?: nullVal() }
|
||||
|
||||
fun isNull(): Val<Boolean> =
|
||||
map { it == null }
|
||||
|
@ -40,7 +40,7 @@ fun <T1, T2, R> map(
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> 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.
|
||||
@ -53,7 +53,7 @@ fun <T1, T2, T3, R> map(
|
||||
v3: Val<T3>,
|
||||
transform: (T1, T2, T3) -> 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
|
||||
@ -66,4 +66,7 @@ fun <T1, T2, R> flatMap(
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> 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 } }
|
||||
|
@ -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<E>(
|
||||
private val dependencies: List<Val<*>>,
|
||||
private vararg val dependencies: Val<*>,
|
||||
) : AbstractListVal<E>(extractObservables = null) {
|
||||
private val _sizeVal = SizeVal()
|
||||
|
||||
|
@ -67,7 +67,7 @@ abstract class AbstractListVal<E>(
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Val<E?> =
|
||||
DependentVal(listOf(this)) { value.firstOrNull() }
|
||||
DependentVal(this) { value.firstOrNull() }
|
||||
|
||||
/**
|
||||
* Does the following in the given order:
|
||||
|
@ -7,9 +7,9 @@ import world.phantasmal.observable.value.Val
|
||||
* ListVal of which the value depends on 0 or more other vals.
|
||||
*/
|
||||
class DependentListVal<E>(
|
||||
dependencies: List<Val<*>>,
|
||||
vararg dependencies: Val<*>,
|
||||
private val computeElements: () -> List<E>,
|
||||
) : AbstractDependentListVal<E>(dependencies) {
|
||||
) : AbstractDependentListVal<E>(*dependencies) {
|
||||
private var _elements: List<E>? = null
|
||||
|
||||
override val elements: List<E> get() = _elements.unsafeAssertNotNull()
|
||||
|
@ -8,9 +8,9 @@ import world.phantasmal.observable.value.Val
|
||||
* Similar to [DependentListVal], except that this val's [computeElements] returns a ListVal.
|
||||
*/
|
||||
class FlatteningDependentListVal<E>(
|
||||
dependencies: List<Val<*>>,
|
||||
vararg dependencies: Val<*>,
|
||||
private val computeElements: () -> ListVal<E>,
|
||||
) : AbstractDependentListVal<E>(dependencies) {
|
||||
) : AbstractDependentListVal<E>(*dependencies) {
|
||||
private var computedVal: ListVal<E>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
|
||||
|
@ -23,6 +23,9 @@ interface ListVal<out E> : Val<List<E>> {
|
||||
fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> =
|
||||
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> =
|
||||
fold(0) { acc, el -> acc + selector(el) }
|
||||
|
||||
@ -30,4 +33,6 @@ interface ListVal<out E> : Val<List<E>> {
|
||||
FilteredListVal(this, predicate)
|
||||
|
||||
fun firstOrNull(): Val<E?>
|
||||
|
||||
operator fun contains(element: @UnsafeVariance E): Boolean = element in value
|
||||
}
|
||||
|
@ -18,4 +18,4 @@ fun <T1, T2, R> flatMapToList(
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> ListVal<R>,
|
||||
): ListVal<R> =
|
||||
FlatteningDependentListVal(listOf(v1, v2)) { transform(v1.value, v2.value) }
|
||||
FlatteningDependentListVal(v1, v2) { transform(v1.value, v2.value) }
|
||||
|
@ -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 <T> createWithValue(value: T): DependentVal<T> {
|
||||
val v = SimpleVal(value)
|
||||
return DependentVal(listOf(v)) { v.value }
|
||||
return DependentVal(v) { v.value }
|
||||
}
|
||||
}
|
||||
|
@ -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 <T> createWithValue(value: T): FlatteningDependentVal<T> {
|
||||
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(
|
||||
|
@ -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 <T> createWithValue(value: T): FlatteningDependentVal<T> {
|
||||
val v = StaticVal(StaticVal(value))
|
||||
return FlatteningDependentVal(listOf(v)) { v.value }
|
||||
return FlatteningDependentVal(v) { v.value }
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ class DependentListValTests : ListValTests {
|
||||
override fun createProvider() = object : ListValTests.Provider {
|
||||
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() {
|
||||
l.add(4)
|
||||
|
@ -14,7 +14,7 @@ class FlatteningDependentListValDependentValEmitsTests : ListValTests {
|
||||
private val dependencyVal = SimpleVal<ListVal<Int>>(nestedVal)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListVal(listOf(dependencyVal)) { dependencyVal.value }
|
||||
FlatteningDependentListVal(dependencyVal) { dependencyVal.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the direct dependency.
|
||||
|
@ -14,7 +14,7 @@ class FlatteningDependentListValNestedValEmitsTests : ListValTests {
|
||||
private val dependentVal = StaticVal<ListVal<Int>>(nestedVal)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListVal(listOf(dependentVal)) { dependentVal.value }
|
||||
FlatteningDependentListVal(dependentVal) { dependentVal.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the nested dependency.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -17,7 +17,21 @@ interface Undo {
|
||||
*/
|
||||
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 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()
|
||||
}
|
||||
|
@ -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<Undo>(NopUndo)
|
||||
private val undos = mutableListVal<Undo>(NopUndo) { arrayOf(it.atSavePoint) }
|
||||
private val _current = mutableVal<Undo>(NopUndo)
|
||||
|
||||
val current: Val<Undo> = _current
|
||||
@ -17,6 +15,12 @@ class UndoManager {
|
||||
val firstUndo: Val<Action?> = current.flatMap { it.firstUndo }
|
||||
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) {
|
||||
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.
|
||||
}
|
||||
|
@ -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<Boolean> = index gt 0
|
||||
|
||||
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 atSavePoint: Val<Boolean> = 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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -64,7 +64,11 @@ class QuestEditorToolbarController(
|
||||
// Saving
|
||||
|
||||
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(
|
||||
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<Nothing>(logger)
|
||||
@ -297,6 +303,8 @@ class QuestEditorToolbarController(
|
||||
URL.revokeObjectURL(url)
|
||||
document.body?.removeChild(a)
|
||||
}
|
||||
|
||||
questEditorStore.questSaved()
|
||||
} catch (e: Throwable) {
|
||||
setResult(
|
||||
PwResult.build<Nothing>(logger)
|
||||
|
@ -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<Unit>()
|
||||
private val _didRedo = emitter<Unit>()
|
||||
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<Boolean> = _inlineStackArgs
|
||||
|
||||
@ -57,8 +48,8 @@ class AsmStore(
|
||||
|
||||
val editingEnabled: Val<Boolean> = questEditorStore.questEditingEnabled
|
||||
|
||||
val didUndo: Observable<Unit> = _didUndo
|
||||
val didRedo: Observable<Unit> = _didRedo
|
||||
val didUndo: Observable<Unit> = undo.didUndo
|
||||
val didRedo: Observable<Unit> = undo.didRedo
|
||||
|
||||
val problems: ListVal<AssemblyProblem> = 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()
|
||||
|
||||
|
@ -58,6 +58,7 @@ class QuestEditorStore(
|
||||
val firstUndo: Val<Action?> = undoManager.firstUndo
|
||||
val canRedo: Val<Boolean> = questEditingEnabled and undoManager.canRedo
|
||||
val firstRedo: Val<Action?> = undoManager.firstRedo
|
||||
val canSaveChanges: Val<Boolean> = !undoManager.allAtSavePoint
|
||||
|
||||
val showCollisionGeometry: Val<Boolean> = _showCollisionGeometry
|
||||
|
||||
@ -233,4 +234,8 @@ class QuestEditorStore(
|
||||
fun setShowCollisionGeometry(show: Boolean) {
|
||||
_showCollisionGeometry.value = show
|
||||
}
|
||||
|
||||
fun questSaved() {
|
||||
undoManager.savePoint()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user