diff --git a/CHANGELOG.md b/CHANGELOG.md index cab52d24c..1bd8a70d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Hot View Reload (/~https://github.com/edvin/tornadofx/issues/96) - `children(nodeList)` builder helper to redirect built children to a specific node list (/~https://github.com/edvin/tornadofx/issues/95) - `buttonbar` builder (/~https://github.com/edvin/tornadofx/issues/95) - `ButtonBar.button` builder (/~https://github.com/edvin/tornadofx/issues/95) diff --git a/src/main/java/tornadofx/App.kt b/src/main/java/tornadofx/App.kt index cd819c3df..d6fc7a8d4 100644 --- a/src/main/java/tornadofx/App.kt +++ b/src/main/java/tornadofx/App.kt @@ -2,6 +2,8 @@ package tornadofx import javafx.application.Application import javafx.beans.property.SimpleStringProperty +import javafx.collections.FXCollections +import javafx.collections.ObservableMap import javafx.scene.Scene import javafx.stage.Stage import kotlin.properties.ReadOnlyProperty @@ -19,6 +21,7 @@ abstract class App : Application() { stage.apply { scene = Scene(view.root) + view.properties["tornadofx.scene"] = scene scene.stylesheets.addAll(FX.stylesheets) titleProperty().bind(view.titleProperty) show() @@ -40,6 +43,9 @@ abstract class SingleViewApp(title: String? = null) : App(), ViewContainer { var stylesheet: Stylesheet? = null override val primaryView = javaClass.kotlin override val titleProperty = SimpleStringProperty(title) + override val properties: ObservableMap + get() = _properties.value + private val _properties = lazy { FXCollections.observableHashMap() } init { FX.components[javaClass.kotlin] = this diff --git a/src/main/java/tornadofx/Component.kt b/src/main/java/tornadofx/Component.kt index 87bf239ab..e32c8fcfa 100644 --- a/src/main/java/tornadofx/Component.kt +++ b/src/main/java/tornadofx/Component.kt @@ -1,5 +1,6 @@ package tornadofx +import javafx.application.Platform import javafx.beans.property.Property import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty @@ -16,6 +17,7 @@ import javafx.scene.input.KeyEvent import javafx.stage.Modality import javafx.stage.Stage import javafx.stage.StageStyle +import java.io.Serializable import java.net.URL import java.nio.file.Files import java.nio.file.Paths @@ -94,6 +96,7 @@ abstract class Component { @Deprecated("Clashes with Region.background, so runAsync is a better name", ReplaceWith("runAsync"), DeprecationLevel.WARNING) fun background(func: () -> T) = task(func) + fun runAsync(func: () -> T) = task(func) infix fun Task.ui(func: (T) -> Unit) = success(func) } @@ -101,14 +104,26 @@ abstract class Component { abstract class Controller : Component(), Injectable interface ViewContainer : Injectable { + val properties: ObservableMap val root: Parent val titleProperty: Property + fun pack(): Serializable? = null + fun unpack(state: Serializable?) { + } } abstract class UIComponent : Component(), ViewContainer { var fxmlLoader: FXMLLoader? = null var modalStage: Stage? = null + private fun tagRoot() { + root.properties["tornadofx.uicomponent"] = this@UIComponent + } + + init { + Platform.runLater { tagRoot() } + } + fun openModal(stageStyle: StageStyle = StageStyle.DECORATED, modality: Modality = Modality.APPLICATION_MODAL, escapeClosesWindow: Boolean = true) { if (modalStage == null) { if (root !is Parent) { @@ -129,10 +144,13 @@ abstract class UIComponent : Component(), ViewContainer { stylesheets.addAll(FX.stylesheets) icons += FX.primaryStage.icons scene = this + properties["tornadofx.scene"] = this } show() + if (FX.reloadStylesheetsOnFocus) reloadStylesheetsOnFocus() + if (FX.reloadViewsOnFocus) reloadViewsOnFocus() } } } diff --git a/src/main/java/tornadofx/FX.kt b/src/main/java/tornadofx/FX.kt index ab7d9e2f2..fa289d1fe 100644 --- a/src/main/java/tornadofx/FX.kt +++ b/src/main/java/tornadofx/FX.kt @@ -5,7 +5,9 @@ import javafx.application.Platform import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty import javafx.collections.FXCollections +import javafx.scene.Scene import javafx.scene.image.Image +import javafx.scene.layout.Pane import javafx.stage.Stage import java.util.* import java.util.concurrent.CountDownLatch @@ -25,6 +27,8 @@ class FX { var dicontainer: DIContainer? = null @JvmStatic var reloadStylesheetsOnFocus = false + @JvmStatic + var reloadViewsOnFocus = false private val _locale: SimpleObjectProperty = object : SimpleObjectProperty() { override fun invalidated() = loadMessages() @@ -87,11 +91,32 @@ class FX { FX.primaryStage = primaryStage FX.application = application if (reloadStylesheetsOnFocus) primaryStage.reloadStylesheetsOnFocus() + if (reloadViewsOnFocus) primaryStage.reloadViewsOnFocus() } @JvmStatic fun find(componentType: Class) = find(componentType.kotlin) + + fun replaceComponent(obsolete: UIComponent) { + if (obsolete is View) components.remove(obsolete.javaClass.kotlin) + val replacement = find(obsolete.javaClass.kotlin) + + val state = obsolete.pack() + replacement.unpack(state) + + if (obsolete.root.parent is Pane) { + (obsolete.root.parent as Pane).children.apply { + val index = indexOf(obsolete.root) + remove(obsolete.root) + add(index, replacement.root) + } + } else { + val scene = obsolete.properties["tornadofx.scene"] as Scene + replacement.properties["tornadofx.scene"] = scene + scene.root = replacement.root + } + } } } @@ -104,6 +129,10 @@ fun reloadStylesheetsOnFocus() { FX.reloadStylesheetsOnFocus = true } +fun reloadViewsOnFocus() { + FX.reloadViewsOnFocus = true +} + fun importStylesheet(stylesheet: String) { val css = FX::class.java.getResource(stylesheet) FX.stylesheets.add(css.toExternalForm()) diff --git a/src/main/java/tornadofx/Nodes.kt b/src/main/java/tornadofx/Nodes.kt index c88e788cd..614e89d55 100644 --- a/src/main/java/tornadofx/Nodes.kt +++ b/src/main/java/tornadofx/Nodes.kt @@ -9,6 +9,7 @@ import javafx.geometry.Insets import javafx.geometry.Pos import javafx.geometry.VPos import javafx.scene.Node +import javafx.scene.Parent import javafx.scene.Scene import javafx.scene.control.* import javafx.scene.control.cell.CheckBoxTableCell @@ -25,6 +26,7 @@ import javafx.util.converter.* import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.util.* import kotlin.reflect.KClass fun TableColumnBase<*, *>.hasClass(className: String) = styleClass.contains(className) @@ -64,9 +66,31 @@ fun Scene.reloadStylesheets() { stylesheets.addAll(styles) } +fun Scene.reloadViews() { + findUIComponents().forEach { FX.replaceComponent(it) } +} + +fun Scene.findUIComponents(): List { + val list = ArrayList() + root.findUIComponents(list) + return list +} + +private fun Parent.findUIComponents(list: MutableList) { + val uicmp = properties["tornadofx.uicomponent"] + if (uicmp is UIComponent) list += uicmp + childrenUnmodifiable.filtered { it is Parent }.forEach { (it as Parent).findUIComponents(list) } +} + fun Stage.reloadStylesheetsOnFocus() { focusedProperty().addListener { obs, old, focused -> - if (focused) scene.reloadStylesheets() + if (focused && FX.initialized.value) scene.reloadStylesheets() + } +} + +fun Stage.reloadViewsOnFocus() { + focusedProperty().addListener { obs, old, focused -> + if (focused && FX.initialized.value) scene.reloadViews() } }