[Editor] Make UndoRedoService context aware

This commit is contained in:
2021-02-06 00:35:35 +01:00
parent 1a8e34f25b
commit 1374f2843b
14 changed files with 123 additions and 31 deletions

View File

@@ -0,0 +1,3 @@
package com.bartlomiejpluta.base.editor.command.context
interface UndoableContext

View File

@@ -0,0 +1,5 @@
package com.bartlomiejpluta.base.editor.command.context
import tornadofx.Scope
class UndoableScope : UndoableContext, Scope()

View File

@@ -1,5 +0,0 @@
package com.bartlomiejpluta.base.editor.command.model
interface Command {
fun execute()
}

View File

@@ -0,0 +1,5 @@
package com.bartlomiejpluta.base.editor.command.model.base
interface Command {
fun execute()
}

View File

@@ -1,4 +1,4 @@
package com.bartlomiejpluta.base.editor.command.model package com.bartlomiejpluta.base.editor.command.model.base
class SimpleCommand<T>( class SimpleCommand<T>(
override val commandName: String, override val commandName: String,

View File

@@ -1,4 +1,4 @@
package com.bartlomiejpluta.base.editor.command.model package com.bartlomiejpluta.base.editor.command.model.base
interface Undoable { interface Undoable {
fun undo() fun undo()

View File

@@ -1,16 +1,18 @@
package com.bartlomiejpluta.base.editor.command.service package com.bartlomiejpluta.base.editor.command.service
import com.bartlomiejpluta.base.editor.command.model.Undoable import com.bartlomiejpluta.base.editor.command.context.UndoableContext
import com.bartlomiejpluta.base.editor.command.model.base.Undoable
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.lang.Integer.toHexString
import java.util.* import java.util.*
@Component @Component
class DefaultUndoRedoService : UndoRedoService { class DefaultUndoRedoService : UndoRedoService {
private val undo: Deque<Undoable> = ArrayDeque() private val undo: Deque<Pair<Undoable, UndoableContext>> = ArrayDeque()
private val redo: Deque<Undoable> = ArrayDeque() private val redo: Deque<Pair<Undoable, UndoableContext>> = ArrayDeque()
override var sizeMax = 30 var sizeMax = 30
set(value) { set(value) {
if (value >= 0) { if (value >= 0) {
for (i in 0 until undo.size - value) { for (i in 0 until undo.size - value) {
@@ -22,21 +24,36 @@ class DefaultUndoRedoService : UndoRedoService {
} }
override fun push(undoable: Undoable) { override fun push(undoable: Undoable) {
push(undoable, GLOBAL_CONTEXT)
}
override fun push(undoable: Undoable, context: UndoableContext) {
if (undo.size == sizeMax) { if (undo.size == sizeMax) {
log.debug("The max size of [undo] list has been reached. Removing the last item...") log.debug("The max size of [undo] stack has been reached. Removing the last item...")
undo.removeLast() undo.removeLast()
} }
log.debug("Pushing item to [undo] list: ${undoable.commandName}") log.debug("Pushing item to [undo] stack: ${undoable.commandName} (ctx: ${toHexString(context.hashCode())})")
undo.push(undoable) undo.push(undoable to context)
redo.clear() redo.clear()
} }
override fun undo() { override fun undo() {
if (undo.isNotEmpty()) { if (undo.isNotEmpty()) {
undo.pop().let { undo.pop().let {
log.debug("Performing undo: ${it.commandName}") log.debug("Performing undo: ${it.first.commandName}")
it.undo() it.first.undo()
redo.push(it)
}
}
}
override fun undo(context: UndoableContext) {
if (undo.isNotEmpty()) {
undo.firstOrNull { it.second === context }?.let {
log.debug("Performing contextual (ctx: ${toHexString(context.hashCode())}) undo: ${it.first.commandName}")
undo.remove(it)
it.first.undo()
redo.push(it) redo.push(it)
} }
} }
@@ -45,26 +62,49 @@ class DefaultUndoRedoService : UndoRedoService {
override fun redo() { override fun redo() {
if (redo.isNotEmpty()) { if (redo.isNotEmpty()) {
redo.pop().let { redo.pop().let {
log.debug("Performing redo: ${it.commandName}") log.debug("Performing redo: ${it.first.commandName}")
it.redo() it.first.redo()
undo.push(it)
}
}
}
override fun redo(context: UndoableContext) {
if (redo.isNotEmpty()) {
redo.firstOrNull { it.second === context }?.let {
log.debug("Performing contextual (ctx: ${toHexString(context.hashCode())}) redo: ${it.first.commandName}")
redo.remove(it)
it.first.redo()
undo.push(it) undo.push(it)
} }
} }
} }
override val lastUndoable: Undoable? override val lastUndoable: Undoable?
get() = undo.last get() = undo.first?.first
override val lastRedoable: Undoable? override val lastRedoable: Undoable?
get() = redo.last get() = redo.first?.first
override val undoCommandName: String override val undoCommandName: String
get() = undo.last?.commandName ?: "" get() = undo.first?.first?.commandName ?: ""
override val redoCommandName: String override val redoCommandName: String
get() = redo.last?.commandName ?: "" get() = redo.first?.first?.commandName ?: ""
override fun lastUndoable(context: UndoableContext) = undo.firstOrNull { it.second === context }?.first
override fun lastRedoable(context: UndoableContext) = redo.firstOrNull { it.second === context }?.first
override fun undoCommandName(context: UndoableContext) =
undo.firstOrNull { it.second === context }?.first?.commandName ?: ""
override fun redoCommandName(context: UndoableContext) =
redo.firstOrNull { it.second === context }?.first?.commandName ?: ""
companion object { companion object {
private val log = LoggerFactory.getLogger(DefaultUndoRedoService::class.java) private val log = LoggerFactory.getLogger(DefaultUndoRedoService::class.java)
private val GLOBAL_CONTEXT = object : UndoableContext {}
} }
} }

View File

@@ -1,15 +1,24 @@
package com.bartlomiejpluta.base.editor.command.service package com.bartlomiejpluta.base.editor.command.service
import com.bartlomiejpluta.base.editor.command.model.Undoable import com.bartlomiejpluta.base.editor.command.context.UndoableContext
import com.bartlomiejpluta.base.editor.command.model.base.Undoable
interface UndoRedoService { interface UndoRedoService {
fun push(undoable: Undoable) fun push(undoable: Undoable)
fun undo() fun undo()
fun redo() fun redo()
fun push(undoable: Undoable, context: UndoableContext)
fun undo(context: UndoableContext)
fun redo(context: UndoableContext)
val lastUndoable: Undoable? val lastUndoable: Undoable?
val lastRedoable: Undoable? val lastRedoable: Undoable?
val undoCommandName: String val undoCommandName: String
val redoCommandName: String val redoCommandName: String
var sizeMax: Int
fun lastUndoable(context: UndoableContext): Undoable?
fun lastRedoable(context: UndoableContext): Undoable?
fun undoCommandName(context: UndoableContext): String
fun redoCommandName(context: UndoableContext): String
} }

View File

@@ -53,7 +53,7 @@ class MapPainter(
} }
private fun beginTrace(event: MapMouseEvent) { private fun beginTrace(event: MapMouseEvent) {
if (event.button == MouseButton.PRIMARY) { if (event.button == MouseButton.PRIMARY && mapVM.selectedLayer >= 0) {
currentTrace = MapPaintingTrace(mapVM, "Paint trace").apply { currentTrace = MapPaintingTrace(mapVM, "Paint trace").apply {
brushVM.forEach { row, column, tile -> brushVM.forEach { row, column, tile ->
paint( paint(

View File

@@ -1,6 +1,6 @@
package com.bartlomiejpluta.base.editor.render.canvas.map package com.bartlomiejpluta.base.editor.render.canvas.map
import com.bartlomiejpluta.base.editor.command.model.Undoable import com.bartlomiejpluta.base.editor.command.model.base.Undoable
import com.bartlomiejpluta.base.editor.model.map.layer.TileLayer import com.bartlomiejpluta.base.editor.model.map.layer.TileLayer
import com.bartlomiejpluta.base.editor.model.tileset.Tile import com.bartlomiejpluta.base.editor.model.tileset.Tile
import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM

View File

@@ -1,5 +1,6 @@
package com.bartlomiejpluta.base.editor.view.main package com.bartlomiejpluta.base.editor.view.main
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.command.service.UndoRedoService import com.bartlomiejpluta.base.editor.command.service.UndoRedoService
import com.bartlomiejpluta.base.editor.controller.map.MapController import com.bartlomiejpluta.base.editor.controller.map.MapController
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
@@ -18,7 +19,7 @@ class MainView : View() {
button("Map 1") { button("Map 1") {
action { action {
val map = mapController.getMap(1) val map = mapController.getMap(1)
tabPane += find<MapFragment>(Scope(), MapFragment::map to map).apply { tabPane += find<MapFragment>(UndoableScope(), MapFragment::map to map).apply {
title = "Map 1" title = "Map 1"
} }
} }
@@ -27,7 +28,7 @@ class MainView : View() {
button("Map 2") { button("Map 2") {
action { action {
val map = mapController.getMap(2) val map = mapController.getMap(2)
tabPane += find<MapFragment>(Scope(), MapFragment::map to map).apply { tabPane += find<MapFragment>(UndoableScope(), MapFragment::map to map).apply {
title = "Map 2" title = "Map 2"
} }
} }

View File

@@ -1,11 +1,18 @@
package com.bartlomiejpluta.base.editor.view.map package com.bartlomiejpluta.base.editor.view.map
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.command.service.UndoRedoService
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
import com.bartlomiejpluta.base.editor.model.map.map.GameMap import com.bartlomiejpluta.base.editor.model.map.map.GameMap
import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.* import tornadofx.*
class MapFragment : Fragment() { class MapFragment : Fragment() {
private val undoRedoService: UndoRedoService by di()
override val scope = super.scope as UndoableScope
val map: GameMap by param() val map: GameMap by param()
private val mapVM = find<GameMapVM>().apply { item = map } private val mapVM = find<GameMapVM>().apply { item = map }
@@ -14,8 +21,25 @@ class MapFragment : Fragment() {
private val layersView = find<MapLayersView>() private val layersView = find<MapLayersView>()
private val tileSetView = find<TileSetView>() private val tileSetView = find<TileSetView>()
override val root = borderpane { override val root = borderpane {
top = toolbar {
button(graphic = FontIcon("fa-undo")) {
shortcut("Ctrl+Z")
action {
undoRedoService.undo(scope)
fire(RedrawMapRequestEvent)
}
}
button(graphic = FontIcon("fa-repeat")) {
shortcut("Ctrl+Shift+Z")
action {
undoRedoService.redo(scope)
fire(RedrawMapRequestEvent)
}
}
}
center = mapView.root center = mapView.root
right = drawer(multiselect = true) { right = drawer(multiselect = true) {

View File

@@ -1,5 +1,7 @@
package com.bartlomiejpluta.base.editor.view.map package com.bartlomiejpluta.base.editor.view.map
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.command.service.UndoRedoService
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
import com.bartlomiejpluta.base.editor.model.map.layer.Layer import com.bartlomiejpluta.base.editor.model.map.layer.Layer
import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM import com.bartlomiejpluta.base.editor.viewmodel.map.GameMapVM
@@ -8,6 +10,10 @@ import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.* import tornadofx.*
class MapLayersView : View() { class MapLayersView : View() {
private val undoRedoService: UndoRedoService by di()
override val scope = super.scope as UndoableScope
private val mapVM = find<GameMapVM>() private val mapVM = find<GameMapVM>()
private var layersPane = TableView(mapVM.layers).apply { private var layersPane = TableView(mapVM.layers).apply {

View File

@@ -1,5 +1,6 @@
package com.bartlomiejpluta.base.editor.view.map package com.bartlomiejpluta.base.editor.view.map
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.command.service.UndoRedoService import com.bartlomiejpluta.base.editor.command.service.UndoRedoService
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
import com.bartlomiejpluta.base.editor.view.component.map.MapPane import com.bartlomiejpluta.base.editor.view.component.map.MapPane
@@ -18,16 +19,19 @@ import tornadofx.scrollpane
class MapView : View() { class MapView : View() {
private val undoRedoService: UndoRedoService by di() private val undoRedoService: UndoRedoService by di()
override val scope = super.scope as UndoableScope
val zoomProperty = SimpleDoubleProperty(1.0) val zoomProperty = SimpleDoubleProperty(1.0)
private val zoom = Scale(1.0, 1.0, 0.0, 0.0).apply { private val zoom = Scale(1.0, 1.0, 0.0, 0.0).apply {
xProperty().bind(zoomProperty) xProperty().bind(zoomProperty)
yProperty().bind(zoomProperty) yProperty().bind(zoomProperty)
} }
private val mapVM = find<GameMapVM>() private val mapVM = find<GameMapVM>()
private val brushVM = find<BrushVM>() private val brushVM = find<BrushVM>()
private val mapPane = MapPane(mapVM, brushVM) { undoRedoService.push(it) } private val mapPane = MapPane(mapVM, brushVM) { undoRedoService.push(it, scope) }
init { init {
brushVM.item = mapVM.tileSet.baseBrush brushVM.item = mapVM.tileSet.baseBrush