[Editor] Enable main view's tab selection on change | implement line selection from compilation logs on opened tabs

This commit is contained in:
2021-02-25 12:04:51 +01:00
parent 1545baab57
commit c9716d438d
5 changed files with 94 additions and 51 deletions

View File

@@ -1,17 +1,18 @@
package com.bartlomiejpluta.base.editor.code.model
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import tornadofx.toProperty
class CodeScope(line: Int, column: Int) : UndoableScope() {
private val requestCaretPositionProperty = (line to column).toProperty()
class CodeScope(private var line: Int, private var column: Int) : UndoableScope() {
var caretDisplacementRequestListener: ((line: Int, column: Int) -> Unit)? = null
set(value) {
field = value
field?.let { it(line, column) }
}
fun setCaretPosition(line: Int, column: Int) {
requestCaretPositionProperty.value = line to column
}
this.line = line
this.column = column
fun addCaretDisplacementRequestListener(listener: (line: Int, column: Int) -> Unit) {
requestCaretPositionProperty.addListener { _, _, position -> listener(position.first, position.second) }
listener(requestCaretPositionProperty.value.first, requestCaretPositionProperty.value.second)
caretDisplacementRequestListener?.let { it(line, column) }
}
}

View File

@@ -28,7 +28,7 @@ class CodeEditorView : View() {
private val editor = CodeEditor(highlighter, codeVM.codeProperty)
init {
scope.addCaretDisplacementRequestListener { line, column ->
scope.caretDisplacementRequestListener = { line, column ->
editor.setCaretPosition(line, column)
}
}

View File

@@ -0,0 +1,7 @@
package com.bartlomiejpluta.base.editor.event
import tornadofx.EventBus
import tornadofx.FXEvent
import tornadofx.Scope
class SelectMainViewTabEvent(val targetScope: Scope) : FXEvent(EventBus.RunOn.ApplicationThread)

View File

@@ -6,6 +6,7 @@ import com.bartlomiejpluta.base.editor.code.model.CodeScope
import com.bartlomiejpluta.base.editor.code.model.FileSystemNode
import com.bartlomiejpluta.base.editor.code.viewmodel.CodeVM
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.event.SelectMainViewTabEvent
import com.bartlomiejpluta.base.editor.image.view.importing.ImportImageFragment
import com.bartlomiejpluta.base.editor.image.viewmodel.ImageAssetDataVM
import com.bartlomiejpluta.base.editor.map.asset.GameMapAsset
@@ -76,24 +77,49 @@ class MainController : Controller() {
}
fun openMap(uid: String) {
if (openItems.count { (_, item) -> item is GameMap && item.uid == uid } == 0) {
openItem<GameMap, UndoableScope>({ it.uid == uid }) {
val map = projectContext.loadMap(uid)
val vm = GameMapVM(map)
val scope = UndoableScope()
setInScope(vm, scope)
openItems[scope] = map
scope to map
}
}
fun openScript(fsNode: FileSystemNode, line: Int = 1, column: Int = 1) {
if (openItems.count { (_, item) -> item is Code && item.file.absolutePath == fsNode.file.absolutePath } == 0) {
val findScript = { script: Code -> script.file.absolutePath == fsNode.file.absolutePath }
val updateExistingScope = { scope: CodeScope -> scope.setCaretPosition(line, column) }
openItem(findScript, updateExistingScope) {
val code = projectContext.loadScript(fsNode.fileProperty)
val vm = CodeVM(code)
val scope = CodeScope(line, column)
setInScope(vm, scope)
openItems[scope] = code
scope to code
}
}
private inline fun <reified T, S : Scope> openItem(
findItem: (item: T) -> Boolean,
updateExistingScope: (S) -> Unit = {},
createItem: () -> Pair<Scope, T>
) {
@Suppress("UNCHECKED_CAST")
val pair = openItems.entries
.filter { (_, item) -> item is T }
.map { (scope, item) -> (scope as S) to (item as T) }
.firstOrNull { (_, item) -> findItem(item) }
if (pair == null) {
val (scope, item) = createItem()
openItems[scope] = item
fire(SelectMainViewTabEvent(scope))
} else {
val scope = pair.first
updateExistingScope(scope)
fire(SelectMainViewTabEvent(scope))
}
}

View File

@@ -6,6 +6,7 @@ import com.bartlomiejpluta.base.editor.code.view.CodeEditorFragment
import com.bartlomiejpluta.base.editor.code.view.CompilerLogsView
import com.bartlomiejpluta.base.editor.code.view.ScriptFilesView
import com.bartlomiejpluta.base.editor.code.viewmodel.CodeVM
import com.bartlomiejpluta.base.editor.event.SelectMainViewTabEvent
import com.bartlomiejpluta.base.editor.event.UpdateCompilationLogEvent
import com.bartlomiejpluta.base.editor.main.controller.MainController
import com.bartlomiejpluta.base.editor.map.model.map.GameMap
@@ -32,6 +33,45 @@ class MainView : View("BASE Game Editor") {
private var compilationLogItem: DrawerItem by singleAssign()
private val tabPane = tabpane {
// FIXME
// For some reason the plain object binding does not work between tabs and mainController.openItems.
// Because of that, the cache openTabs map has been created and the binding is done by following listener:
mainController.openItems.addListener(MapChangeListener {
when {
it.wasAdded() -> createTab(it.key, it.valueAdded).let { tab ->
openTabs[it.key] = tab
tabs += tab
}
it.wasRemoved() -> {
val tab = openTabs[it.key]
openTabs.remove(it.key)
// FIXME
// This ugly hack prevents from double-firing the CLOSED EVENT
// when user tries to close tab from the UI (cross sign).
// When user clicks the close button, the event is fired automatically,
// which removes the item from mainController.openItems.
// Right here, this listener would fire the event once again,
// so essentially we need to check if the tab exists in TabPane.tabs
// (which means the tab wasn't be closed from UI). If so, we need to fire
// the event manually since it won't be fired by TabPane.
// Otherwise, if the tab does not exist in tabs anymore, the tab
// was closed from UI and likely the event has been fired automatically.
// The same goes for the TAB_CLOSE_REQUEST event.
if (tab in tabs) {
Event.fireEvent(tab, Event(Tab.CLOSED_EVENT))
Event.fireEvent(tab, Event(Tab.TAB_CLOSE_REQUEST_EVENT))
}
tabs -= tab
}
}
})
}
init {
projectContext.projectProperty.addListener { _, _, project ->
val projectName = project?.let { " :: ${it.name} (${it.sourceDirectory.absolutePath})" } ?: ""
@@ -41,49 +81,18 @@ class MainView : View("BASE Game Editor") {
subscribe<UpdateCompilationLogEvent> {
compilationLogItem.expanded = true
}
subscribe<SelectMainViewTabEvent> { event ->
openTabs[event.targetScope]?.let {
tabPane.selectionModel.select(it)
}
}
}
override val root = borderpane {
top = mainMenuView.root
center = tabpane {
// FIXME
// For some reason the plain object binding does not work between tabs and mainController.openItems.
// Because of that, the cache openTabs map has been created and the binding is done by following listener:
mainController.openItems.addListener(MapChangeListener {
when {
it.wasAdded() -> createTab(it.key, it.valueAdded).let { tab ->
openTabs[it.key] = tab
tabs += tab
}
it.wasRemoved() -> {
val tab = openTabs[it.key]
openTabs.remove(it.key)
// FIXME
// This ugly hack prevents from double-firing the CLOSED EVENT
// when user tries to close tab from the UI (cross sign).
// When user clicks the close button, the event is fired automatically,
// which removes the item from mainController.openItems.
// Right here, this listener would fire the event once again,
// so essentially we need to check if the tab exists in TabPane.tabs
// (which means the tab wasn't be closed from UI). If so, we need to fire
// the event manually since it won't be fired by TabPane.
// Otherwise, if the tab does not exist in tabs anymore, the tab
// was closed from UI and likely the event has been fired automatically.
// The same goes for the TAB_CLOSE_REQUEST event.
if (tab in tabs) {
Event.fireEvent(tab, Event(Tab.CLOSED_EVENT))
Event.fireEvent(tab, Event(Tab.TAB_CLOSE_REQUEST_EVENT))
}
tabs -= tab
}
}
})
}
center = tabPane
left = drawer(multiselect = true) {
item("Code", expanded = false) {