All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tools.aqua.bgw.builder.NodeBuilder.kt Maven / Gradle / Ivy

There is a newer version: 0.5
Show newest version
/*
 *    Copyright 2021 The BoardGameWork Authors
 *    SPDX-License-Identifier: Apache-2.0
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package tools.aqua.bgw.builder

import javafx.event.EventHandler
import javafx.scene.input.MouseEvent
import javafx.scene.layout.Region
import javafx.scene.layout.StackPane
import tools.aqua.bgw.builder.DragDropHelper.Companion.findActiveComponentsBelowMouse
import tools.aqua.bgw.builder.FXConverters.Companion.toFXFontCSS
import tools.aqua.bgw.builder.FXConverters.Companion.toKeyEvent
import tools.aqua.bgw.builder.FXConverters.Companion.toMouseEvent
import tools.aqua.bgw.components.ComponentView
import tools.aqua.bgw.components.DynamicComponentView
import tools.aqua.bgw.components.StaticComponentView
import tools.aqua.bgw.components.container.GameComponentContainer
import tools.aqua.bgw.components.gamecomponentviews.GameComponentView
import tools.aqua.bgw.components.layoutviews.GridPane
import tools.aqua.bgw.components.layoutviews.LayoutView
import tools.aqua.bgw.components.layoutviews.Pane
import tools.aqua.bgw.components.uicomponents.UIComponent
import tools.aqua.bgw.core.BoardGameScene
import tools.aqua.bgw.core.MenuScene
import tools.aqua.bgw.core.Scene
import tools.aqua.bgw.event.DragEvent
import tools.aqua.bgw.exception.IllegalInheritanceException
import tools.aqua.bgw.util.Coordinate
import kotlin.math.min

/**
 * NodeBuilder.
 * Factory for all BGW nodes.
 */
internal class NodeBuilder {
	companion object {
		/**
		 * Switches between top level component types.
		 */
		internal fun build(scene: Scene, componentView: ComponentView): Region {
			val node = when (componentView) {
				is GameComponentContainer ->
					ContainerNodeBuilder.buildContainer(scene, componentView)
				is GameComponentView ->
					ComponentNodeBuilder.buildGameComponent(componentView)
				is LayoutView ->
					LayoutNodeBuilder.buildLayoutView(scene, componentView)
				is UIComponent ->
					UINodeBuilder.buildUIComponent(componentView)
				is StaticComponentView<*> ->
					throw IllegalInheritanceException(componentView, StaticComponentView::class.java)
				is DynamicComponentView ->
					throw IllegalInheritanceException(componentView, DynamicComponentView::class.java)
				else ->
					throw IllegalInheritanceException(componentView, ComponentView::class.java)
			}
			val background = VisualBuilder.build(componentView)
			val stackPane = StackPane(background, node).apply { isPickOnBounds = false }

			//JavaFX -> Framework
			componentView.registerEvents(stackPane, node, scene)
			
			//Framework -> JavaFX
			componentView.registerObservers(stackPane, node, background)
			
			//Register in componentsMap
			scene.componentsMap[componentView] = stackPane
			
			return stackPane
		}
		
		/**
		 * Registers events.
		 */
		private fun ComponentView.registerEvents(stackPane: StackPane, node: Region, scene: Scene) {
			stackPane.onDragDetected = EventHandler {
				if (this is DynamicComponentView && isDraggable && scene.draggedDataProperty.value == null) {
					onDragDetected(scene, it)
				}
			}
			
			node.setOnMouseClicked { onMouseClicked?.invoke(it.toMouseEvent()) }
			node.setOnMousePressed { onMousePressed?.invoke(it.toMouseEvent()) }
			node.setOnMouseEntered { onMouseEntered?.invoke(it.toMouseEvent()) }
			node.setOnMouseExited { onMouseExited?.invoke(it.toMouseEvent()) }
			
			node.setOnKeyPressed { onKeyPressed?.invoke(it.toKeyEvent()) }
			node.setOnKeyReleased { onKeyReleased?.invoke(it.toKeyEvent()) }
			node.setOnKeyTyped { onKeyTyped?.invoke(it.toKeyEvent()) }
		}
		
		private fun DynamicComponentView.onDragDetected(scene: Scene, e: MouseEvent) {
			val mouseStartCoord = Coordinate(
				xCoord = e.sceneX / Frontend.sceneScale,
				yCoord = e.sceneY / Frontend.sceneScale
			)

			val pathToChild = scene.findPathToChild(this)
			
			var posStartCoord = Coordinate(
				xCoord = pathToChild.sumOf { t -> t.posX },
				yCoord = pathToChild.sumOf { t -> t.posY }
			)
			
			val rollback: (() -> Unit) = when (val parent = pathToChild[1]) {
				is GameComponentContainer<*> -> {
					parent.findRollback(this as GameComponentView)
				}
				is GridPane<*> -> {
					//calculate position in grid
					posStartCoord += parent.getChildPosition(this)!!
					
					//add layout from center bias
					if (parent.layoutFromCenter) {
						posStartCoord -= Coordinate(parent.width / 2, parent.height / 2)
					}
					
					parent.findRollback(this)
				}
				is Pane<*> -> {
					parent.findRollback(this)
				}
				scene.rootNode -> {
					findRollbackOnRoot(scene)
				}
				else -> {
					{}
				}
			}
			
			val relativeParentRotation = if (pathToChild.size > 1)
				pathToChild.drop(1).sumOf { t -> t.rotation }
			else
				0.0
			
			val dragDataObject = DragDataObject(
				this,
				scene.componentsMap[this]!!,
				mouseStartCoord,
				posStartCoord,
				relativeParentRotation,
				rollback
			)
			val newCoords = DragDropHelper.transformCoordinatesToScene(e, dragDataObject)
			
			removeFromParent()
			posX = newCoords.xCoord
			posY = newCoords.yCoord
			scene.draggedDataProperty.value = dragDataObject
			
			scene.dragTargetsBelowMouse.clear()
			scene.dragTargetsBelowMouse.addAll(scene.findActiveComponentsBelowMouse(e.sceneX, e.sceneY))
			
			isDragged = true
			onDragGestureStarted?.invoke(DragEvent(this))
			
		}
		
		/**
		 * Calculates rollback for [GameComponentContainer]s.
		 */
		private fun GameComponentContainer<*>.findRollback(component: GameComponentView): (() -> Unit) {
			val index = observableComponents.indexOf(component)
			val initialX = posX
			val initialY = posY
			
			return {
				posX = initialX
				posY = initialY
				@Suppress("UNCHECKED_CAST")
				(this as GameComponentContainer).add(
					component,
					min(observableComponents.size, index)
				)
			}
		}
		
		/**
		 * Calculates rollback for [GridPane]s.
		 */
		private fun GridPane<*>.findRollback(component: ComponentView): (() -> Unit) {
			val e = grid.find { iteratorElement ->
				iteratorElement.component == component
			} ?: return {}
			
			val initialX = e.component!!.posX
			val initialY = e.component.posY
			val initialColumnIndex = e.columnIndex
			val initialRowIndex = e.rowIndex
			
			return {
				posX = initialX
				posY = initialY
				@Suppress("UNCHECKED_CAST")
				(parent as GridPane)[initialColumnIndex, initialRowIndex] =
					this@findRollback as ComponentView
			}
		}
		
		/**
		 * Calculates rollback for [Pane]s.
		 */
		private fun Pane<*>.findRollback(component: ComponentView): (() -> Unit) {
			val index = observableComponents.indexOf(component)
			val initialX = posX
			val initialY = posY
			
			return {
				posX = initialX
				posY = initialY
				@Suppress("UNCHECKED_CAST")
				(parent as Pane)
					.add(this as ComponentView, min(observableComponents.size, index))
			}
		}
		
		private fun DynamicComponentView.findRollbackOnRoot(scene: Scene): (() -> Unit) {
			val initialX = posX
			val initialY = posY
			return {
				when (scene) {
					is BoardGameScene -> {
						posX = initialX
						posY = initialY
						scene.addComponents(this)
					}
					is MenuScene -> {
						throw RuntimeException("DynamicView $this should not be contained in a MenuScene.")
					}
				}
			}
		}
		
		/**
		 * Registers observers.
		 */
		@Suppress("DuplicatedCode")
		private fun ComponentView.registerObservers(stackPane: StackPane, node: Region, background: Region) {
			posXProperty.setGUIListenerAndInvoke(posX) { _, nV ->
				stackPane.layoutX = nV - if (layoutFromCenter) width / 2 else 0.0
			}
			posYProperty.setGUIListenerAndInvoke(posY) { _, nV ->
				stackPane.layoutY = nV - if (layoutFromCenter) height / 2 else 0.0
			}
			scaleXProperty.setGUIListenerAndInvoke(scaleX) { _, nV ->
				stackPane.scaleX = nV
			}
			scaleYProperty.setGUIListenerAndInvoke(scaleY) { _, nV ->
				stackPane.scaleY = nV
			}
			
			rotationProperty.setGUIListenerAndInvoke(rotation) { _, nV -> stackPane.rotate = nV }
			opacityProperty.setGUIListenerAndInvoke(opacity) { _, nV -> stackPane.opacity = nV }
			
			heightProperty.setGUIListenerAndInvoke(height) { _, nV ->
				node.prefHeight = nV
				background.prefHeight = nV
			}
			widthProperty.setGUIListenerAndInvoke(width) { _, nV ->
				node.prefWidth = nV
				background.prefWidth = nV
			}
			
			isVisibleProperty.setGUIListenerAndInvoke(isVisible) { _, nV ->
				node.isVisible = nV
				background.isVisible = nV
			}
			isDisabledProperty.setGUIListenerAndInvoke(isDisabled) { _, nV ->
				node.isDisable = nV
				background.isDisable = nV
			}

			//TODO: component style needs to update when internal css changes
			if (this is UIComponent) {
				backgroundStyleProperty.setGUIListenerAndInvoke(backgroundStyle) { _, nV ->
					if (nV.isNotEmpty())
						background.style = nV
				}
				
				componentStyleProperty.setGUIListenerAndInvoke(componentStyle) { _,_ -> updateStyle(node) }
				fontProperty.setGUIListenerAndInvoke(font) { _,_ -> updateStyle(node) }
			}
		}
		
		/**
		 * Updates nodes style property
		 */
		private fun UIComponent.updateStyle(node: Region) {
			node.style = this.internalCSS + this.font.toFXFontCSS() + componentStyle
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy