
tornadofx.Nodes.kt Maven / Gradle / Ivy
package tornadofx
import com.sun.javafx.scene.control.skin.TableColumnHeader
import javafx.application.Platform
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections.observableArrayList
import javafx.collections.ObservableList
import javafx.event.EventTarget
import javafx.geometry.HPos
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
import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.InputEvent
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import javafx.scene.input.MouseEvent
import javafx.scene.layout.*
import javafx.stage.Modality
import javafx.stage.Stage
import javafx.util.Callback
import javafx.util.StringConverter
import javafx.util.converter.*
import tornadofx.osgi.OSGIConsole
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)
fun TableColumnBase<*, *>.addClass(className: String) = styleClass.add(className)
fun TableColumnBase<*, *>.removeClass(className: String) = styleClass.remove(className)
fun TableColumnBase<*, *>.toggleClass(className: String, predicate: Boolean) {
if (predicate) {
if (!hasClass(className)) addClass(className)
} else {
removeClass(className)
}
}
fun Node.hasClass(className: String) = styleClass.contains(className)
fun T.addClass(className: String): T {
styleClass.add(className); return this
}
fun T.removeClass(className: String): T {
styleClass.remove(className); return this
}
fun T.toggleClass(className: String, predicate: Boolean): T {
if (predicate) {
if (!hasClass(className)) addClass(className)
} else {
removeClass(className)
}
return this
}
fun Node.getToggleGroup(): ToggleGroup? = properties["tornadofx.togglegroup"] as ToggleGroup?
fun Node.tooltip(text: String? = null, graphic: Node? = null, op: (Tooltip.() -> Unit)? = null): Tooltip {
val newToolTip = Tooltip(text)
graphic?.apply { newToolTip.graphic = this }
if (op != null) newToolTip.op()
if (this is Control) tooltip = newToolTip else Tooltip.install(this, newToolTip)
return newToolTip
}
fun Scene.reloadStylesheets() {
val styles = stylesheets.toMutableList()
stylesheets.clear()
styles.toTypedArray().forEachIndexed { i, s ->
if (s.startsWith("css://")) {
val b = StringBuilder()
val queryPairs = mutableListOf()
if (s.contains("?")) {
val urlAndQuery = s.split(Regex("\\?"), 2)
b.append(urlAndQuery[0])
val query = urlAndQuery[1]
val pairs = query.split("&")
pairs.filterNot { it.startsWith("squash=") }.forEach { queryPairs.add(it) }
} else {
b.append(s)
}
queryPairs.add("squash=${System.currentTimeMillis()}")
b.append("?").append(queryPairs.joinToString("&"))
styles[i] = b.toString()
}
}
stylesheets.addAll(styles)
}
fun Scene.reloadViews() {
if (properties["javafx.layoutdebugger"] == null) {
findUIComponents().forEach {
if (it.reloadInit) FX.replaceComponent(it)
it.reloadInit = true
}
}
}
fun Scene.findUIComponents(): List {
val list = ArrayList()
root.findUIComponents(list)
return list
}
/**
* Aggregate UIComponents under the given parent. Nested UIComponents
* are not aggregated, but they are removed from the FX.components map
* so that they would be reloaded when the parent is reloaded.
*
* This means that nested UIComponents would loose their state, because
* the pack/unpack functions will not be called for these views. This should
* be improved in a future version.
*/
private fun Parent.findUIComponents(list: MutableList) {
val uicmp = uiComponent()
if (uicmp is UIComponent) {
list += uicmp
childrenUnmodifiable.asSequence().filterIsInstance().forEach { it.clearViews() }
} else {
childrenUnmodifiable.asSequence().filterIsInstance().forEach { it.findUIComponents(list) }
}
}
private fun Parent.clearViews() {
val uicmp = uiComponent()
if (uicmp is View) {
FX.components.remove(uicmp.javaClass.kotlin)
} else {
childrenUnmodifiable.asSequence().filterIsInstance().forEach { it.clearViews() }
}
}
fun Stage.reloadStylesheetsOnFocus() {
focusedProperty().addListener { obs, old, focused ->
if (focused && FX.initialized.value) scene?.reloadStylesheets()
}
}
fun Stage.hookGlobalShortcuts() {
addEventFilter(KeyEvent.KEY_PRESSED) {
if (FX.layoutDebuggerShortcut?.match(it) ?: false)
LayoutDebugger.debug(scene)
else if (FX.osgiDebuggerShortcut?.match(it) ?: false && FX.osgiAvailable)
find(OSGIConsole::class).openModal(modality = Modality.NONE)
}
}
fun Stage.reloadViewsOnFocus() {
focusedProperty().addListener { obs, old, focused ->
if (focused && FX.initialized.value) scene?.reloadViews()
}
}
fun Pane.reloadStylesheets() {
val styles = stylesheets.toMutableList()
stylesheets.clear()
stylesheets.addAll(styles)
}
infix fun Node.addTo(pane: EventTarget) = pane.addChildIfPossible(this)
fun Pane.replaceChildren(vararg uiComponents: UIComponent) =
this.replaceChildren(*(uiComponents.map { it.root }.toTypedArray()))
fun EventTarget.replaceChildren(vararg node: Node) {
val children = getChildList() ?: throw IllegalArgumentException("This node doesn't have a child list")
children.clear()
children.addAll(node)
}
operator fun EventTarget.plusAssign(node: Node) {
addChildIfPossible(node)
}
fun T.replaceChildren(op: T.() -> Unit) {
getChildList()?.clear()
op(this)
}
@Deprecated("Just an alias for += SomeType::class", ReplaceWith("this += SomeType::class"), DeprecationLevel.WARNING)
@JvmName("addView")
inline fun EventTarget.add(type: KClass): Unit = plusAssign(find(type).root)
@JvmName("addFragment")
inline fun EventTarget.add(type: KClass): Unit = plusAssign(find(type).root)
fun EventTarget.add(node: Node) = plusAssign(node)
@JvmName("plusView")
operator fun EventTarget.plusAssign(type: KClass): Unit = plusAssign(find(type).root)
@JvmName("plusFragment")
operator fun EventTarget.plusAssign(type: KClass) = plusAssign(find(type).root)
operator fun EventTarget.plusAssign(view: UIComponent) {
if (this is UIComponent) {
root += view
} else {
this += view.root
}
}
var Region.useMaxWidth: Boolean
get() = maxWidth == Double.MAX_VALUE
set(value) = if (value) maxWidth = Double.MAX_VALUE else Unit
var Region.useMaxHeight: Boolean
get() = maxHeight == Double.MAX_VALUE
set(value) = if (value) maxHeight = Double.MAX_VALUE else Unit
var Region.useMaxSize: Boolean
get() = maxWidth == Double.MAX_VALUE && maxHeight == Double.MAX_VALUE
set(value) = if (value) {
useMaxWidth = true; useMaxHeight = true
} else Unit
var Region.usePrefWidth: Boolean
get() = width == prefWidth
set(value) = if (value) setMinWidth(Button.USE_PREF_SIZE) else Unit
var Region.usePrefHeight: Boolean
get() = height == prefHeight
set(value) = if (value) setMinHeight(Button.USE_PREF_SIZE) else Unit
var Region.usePrefSize: Boolean
get() = maxWidth == Double.MAX_VALUE && maxHeight == Double.MAX_VALUE
set(value) = if (value) setMinSize(Button.USE_PREF_SIZE, Button.USE_PREF_SIZE) else Unit
fun TableView.resizeColumnsToFitContent(resizeColumns: List> = columns, maxRows: Int = 50, afterResize: (() -> Unit)? = null) {
val doResize = {
try {
val resizer = skin.javaClass.getDeclaredMethod("resizeColumnToFitContent", TableColumn::class.java, Int::class.java)
resizer.isAccessible = true
resizeColumns.forEach { resizer.invoke(skin, it, maxRows) }
afterResize?.invoke()
} catch (ex: Exception) {
// Silent for now, it is usually run multiple times
//log.warning("Unable to resize columns to content: ${columns.map { it.text }.joinToString(", ")}")
}
}
if (skin == null) Platform.runLater { doResize() } else doResize()
}
fun TreeTableView.resizeColumnsToFitContent(resizeColumns: List> = columns, maxRows: Int = 50, afterResize: (() -> Unit)? = null) {
val doResize = {
val resizer = skin.javaClass.getDeclaredMethod("resizeColumnToFitContent", TreeTableColumn::class.java, Int::class.java)
resizer.isAccessible = true
resizeColumns.forEach { resizer.invoke(skin, it, maxRows) }
afterResize?.invoke()
}
if (skin == null) Platform.runLater { doResize() } else doResize()
}
fun TableView.selectWhere(scrollTo: Boolean = true, condition: (T) -> Boolean) {
items.asSequence().filter(condition)
.forEach {
selectionModel.select(it)
if (scrollTo) scrollTo(it)
}
}
fun TableView.moveToTopWhere(backingList: ObservableList = items, select: Boolean = true, predicate: (T) -> Boolean) {
if (select) selectionModel.clearSelection()
backingList.asSequence().filter(predicate).toList().asSequence().forEach {
backingList.remove(it)
backingList.add(0, it)
if (select) selectionModel.select(it)
}
}
fun TableView.moveToBottomWhere(backingList: ObservableList = items, select: Boolean = true, predicate: (T) -> Boolean) {
val end = backingList.size - 1
if (select) selectionModel.clearSelection()
backingList.asSequence().filter(predicate).toList().asSequence().forEach {
backingList.remove(it)
backingList.add(end, it)
if (select) selectionModel.select(it)
}
}
val TableView.selectedItem: T?
get() = this.selectionModel.selectedItem
val TreeTableView.selectedItem: T?
get() = this.selectionModel.selectedItem?.value
val TreeView.selectedValue: T?
get() = this.selectionModel.selectedItem?.value
fun TableView.selectFirst() = selectionModel.selectFirst()
fun TreeView.selectFirst() = selectionModel.selectFirst()
fun TreeTableView.selectFirst() = selectionModel.selectFirst()
val ListView.selectedItem: T?
get() = selectionModel.selectedItem
val ComboBox.selectedItem: T?
get() = selectionModel.selectedItem
fun TableView.onSelectionChange(func: (S?) -> Unit) =
selectionModel.selectedItemProperty().addListener({ observable, oldValue, newValue -> func(newValue) })
class TableColumnCellCache(private val cacheProvider: TableCell.(T) -> Node) {
private val store = mutableMapOf()
fun getOrCreateNode(cell: TableCell, value: T) = store.getOrPut(value, { cacheProvider(cell, value) })
}
/**
* Calculate a unique Node per item and set this Node as the graphic of the TableCell.
*
* To support this feature, a custom cellFactory is automatically installed, unless an already
* compatible cellFactory is found. The cellFactories installed via #cellFormat already knows
* how to retrieve cached values.
*/
fun TableColumn.cellCache(cachedGraphicProvider: TableCell.(T) -> Node) {
properties["tornadofx.cellCache"] = TableColumnCellCache(cachedGraphicProvider)
// Install a cache capable cellFactory it none is present. The default cellFormat factory will do.
if (properties["tornadofx.cellCacheCapable"] != true) {
cellFormat { }
}
}
@Suppress("UNCHECKED_CAST")
fun TableColumn.cellFormat(formatter: TableCell.(T) -> Unit) {
properties["tornadofx.cellCacheCapable"] = true
cellFactory = Callback { column: TableColumn ->
object : TableCell() {
override fun updateItem(item: T, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
text = null
graphic = null
} else {
// Consult the cell cache before calling the formatter function
val cellCache = [email protected]["tornadofx.cellCache"]
if (cellCache is TableColumnCellCache<*, *>) {
graphic = (cellCache as TableColumnCellCache).getOrCreateNode(this, item)
}
formatter(this, item)
}
}
}
}
}
fun ComboBox.cellFormat(formatter: ListCell.(T) -> Unit) {
cellFactory = Callback { listView: ListView ->
object : ListCell() {
override fun updateItem(item: T, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
text = null
graphic = null
} else {
formatter(this, item)
}
}
}
}
}
fun TableColumn.cellDecorator(decorator: TableCell.(T) -> Unit) {
val originalFactory = cellFactory
cellFactory = Callback { column: TableColumn ->
val cell = originalFactory.call(column)
cell.itemProperty().addListener { obs, oldValue, newValue -> decorator(cell, newValue) }
cell
}
}
fun TreeView.cellFormat(formatter: (TreeCell.(S) -> Unit)) {
cellFactory = Callback {
object : TreeCell() {
override fun updateItem(item: S?, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
text = null
graphic = null
} else {
formatter(this, item)
}
}
}
}
}
fun TreeTableColumn.cellFormat(formatter: (TreeTableCell.(T) -> Unit)) {
cellFactory = Callback { column: TreeTableColumn ->
object : TreeTableCell() {
override fun updateItem(item: T, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
text = null
graphic = null
} else {
formatter(this, item)
}
}
}
}
}
fun ListView.cellFormat(formatter: (ListCell.(T) -> Unit)) {
cellFactory = Callback {
object : ListCell() {
override fun updateItem(item: T, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
with(textProperty()) {
if (isBound) unbind()
value = null
}
with(graphicProperty()) {
if (isBound) unbind()
value = null
}
} else {
formatter(this, item)
}
}
}
}
}
/**
* Execute action when the enter key is pressed or the mouse is clicked
* @param clickCount The number of mouse clicks to trigger the action
* *
* @param action The action to execute on select
*/
fun TableView.onUserSelect(clickCount: Int = 2, action: (T) -> Unit) {
val isSelected = { event: InputEvent ->
event.target.isInsideTableRow() && !selectionModel.isEmpty
}
addEventFilter(MouseEvent.MOUSE_CLICKED) { event ->
if (event.clickCount == clickCount && isSelected(event))
action(selectedItem!!)
}
addEventFilter(KeyEvent.KEY_PRESSED) { event ->
if (event.code == KeyCode.ENTER && !event.isMetaDown && isSelected(event))
action(selectedItem!!)
}
}
/**
* Execute action when the enter key is pressed or the mouse is clicked
* @param clickCount The number of mouse clicks to trigger the action
* *
* @param action The action to execute on select
*/
fun TreeTableView.onUserSelect(clickCount: Int = 2, action: (T) -> Unit) {
val isSelected = { event: InputEvent ->
event.target.isInsideTableRow() && !selectionModel.isEmpty
}
addEventFilter(MouseEvent.MOUSE_CLICKED) { event ->
if (event.clickCount == clickCount && isSelected(event))
action(selectedItem!!)
}
addEventFilter(KeyEvent.KEY_PRESSED) { event ->
if (event.code == KeyCode.ENTER && !event.isMetaDown && isSelected(event))
action(selectedItem!!)
}
}
val TableCell.rowItem: S get() = tableView.items[index]
val TreeTableCell.rowItem: S get() = treeTableView.getTreeItem(index).value
fun SortedFilteredList.asyncItems(func: () -> Collection) =
task { func() } success { items.setAll(it) }
fun TableView.asyncItems(func: () -> Collection) =
task { func() } success { if (items == null) items = observableArrayList(it) else items.setAll(it) }
fun ListView.asyncItems(func: () -> Collection) =
task { func() } success { if (items == null) items = observableArrayList(it) else items.setAll(it) }
fun ComboBox.asyncItems(func: () -> Collection) =
task { func() } success { if (items == null) items = observableArrayList(it) else items.setAll(it) }
fun TreeView.onUserSelect(action: (T) -> Unit) {
selectionModel.selectedItemProperty().addListener { obs, old, new ->
if (new != null && new.value != null)
action(new.value)
}
}
fun TableView.onUserDelete(action: (T) -> Unit) {
addEventFilter(KeyEvent.KEY_PRESSED, { event ->
if (event.code == KeyCode.BACK_SPACE && selectedItem != null)
action(selectedItem!!)
})
}
fun ListView.onUserDelete(action: (T) -> Unit) {
addEventFilter(KeyEvent.KEY_PRESSED, { event ->
if (event.code == KeyCode.BACK_SPACE && selectedItem != null)
action(selectedItem!!)
})
}
fun TreeView.onUserDelete(action: (T) -> Unit) {
addEventFilter(KeyEvent.KEY_PRESSED, { event ->
if (event.code == KeyCode.BACK_SPACE && selectionModel.selectedItem?.value != null)
action(selectedValue!!)
})
}
/**
* Execute action when the enter key is pressed or the mouse is clicked
* @param clickCount The number of mouse clicks to trigger the action
* *
* @param action The runnable to execute on select
*/
fun ListView.onUserSelect(clickCount: Int = 2, action: (T) -> Unit) {
addEventFilter(MouseEvent.MOUSE_CLICKED) { event ->
if (event.clickCount == clickCount && selectedItem != null)
action(selectedItem!!)
}
addEventFilter(KeyEvent.KEY_PRESSED) { event ->
if (event.code == KeyCode.ENTER && !event.isMetaDown && selectedItem != null)
action(selectedItem!!)
}
}
/**
* Did the event occur inside a TableRow or TreeTableRow?
*/
fun EventTarget.isInsideTableRow(): Boolean {
if (this !is Node)
return false
if (this is TableColumnHeader)
return false
if (this is TableRow<*> || this is TableView<*> || this is TreeTableRow<*> || this is TreeTableView<*>)
return true
if (this.parent != null)
return this.parent.isInsideTableRow()
return false
}
/**
* Access BorderPane constraints to manipulate and apply on this control
*/
fun T.borderpaneConstraints(op: (BorderPaneConstraint.() -> Unit)): T {
val bpc = BorderPaneConstraint()
bpc.op()
return bpc.applyToNode(this)
}
class BorderPaneConstraint(
override var margin: Insets = Insets(0.0, 0.0, 0.0, 0.0),
var alignment: Pos? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin.let { BorderPane.setMargin(node, it) }
alignment?.let { BorderPane.setAlignment(node, it) }
return node
}
}
/**
* Access GridPane constraints to manipulate and apply on this control
*/
fun T.gridpaneConstraints(op: (GridPaneConstraint.() -> Unit)): T {
val gpc = GridPaneConstraint()
gpc.op()
return gpc.applyToNode(this)
}
class GridPaneConstraint(
var columnIndex: Int? = null,
var rowIndex: Int? = null,
var hGrow: Priority? = null,
var vGrow: Priority? = null,
override var margin: Insets = Insets(0.0, 0.0, 0.0, 0.0),
var fillHeight: Boolean? = null,
var fillWidth: Boolean? = null,
var hAlignment: HPos? = null,
var vAlignment: VPos? = null,
var columnSpan: Int? = null,
var rowSpan: Int? = null
) : MarginableConstraints() {
var vhGrow: Priority? = null
set(value) {
vGrow = value
hGrow = value
field = value
}
var fillHeightWidth: Boolean? = null
set(value) {
fillHeight = value
fillWidth = value
field = value
}
fun columnRowIndex(columnIndex: Int, rowIndex: Int) {
this.columnIndex = columnIndex
this.rowIndex = rowIndex
}
fun fillHeightWidth(fill: Boolean) {
fillHeight = fill
fillWidth = fill
}
fun applyToNode(node: T): T {
columnIndex?.let { GridPane.setColumnIndex(node, it) }
rowIndex?.let { GridPane.setRowIndex(node, it) }
hGrow?.let { GridPane.setHgrow(node, it) }
vGrow?.let { GridPane.setVgrow(node, it) }
margin.let { GridPane.setMargin(node, it) }
fillHeight?.let { GridPane.setFillHeight(node, it) }
fillWidth?.let { GridPane.setFillWidth(node, it) }
hAlignment?.let { GridPane.setHalignment(node, it) }
vAlignment?.let { GridPane.setValignment(node, it) }
columnSpan?.let { GridPane.setColumnSpan(node, it) }
rowSpan?.let { GridPane.setRowSpan(node, it) }
return node
}
}
fun T.vboxConstraints(op: (VBoxConstraint.() -> Unit)): T {
val c = VBoxConstraint()
c.op()
return c.applyToNode(this)
}
class VBoxConstraint(
override var margin: Insets = Insets(0.0, 0.0, 0.0, 0.0),
var vGrow: Priority? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin.let { VBox.setMargin(node, it) }
vGrow?.let { VBox.setVgrow(node, it) }
return node
}
}
fun T.hboxConstraints(op: (HBoxConstraint.() -> Unit)): T {
val c = HBoxConstraint()
c.op()
return c.applyToNode(this)
}
class HBoxConstraint(
override var margin: Insets = Insets(0.0, 0.0, 0.0, 0.0),
var hGrow: Priority? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin.let { HBox.setMargin(node, it) }
hGrow?.let { HBox.setHgrow(node, it) }
return node
}
}
fun T.anchorpaneConstraints(op: AnchorPaneConstraint.() -> Unit): T {
val c = AnchorPaneConstraint()
c.op()
return c.applyToNode(this)
}
class AnchorPaneConstraint(
var topAnchor: Double? = null,
var rightAnchor: Double? = null,
var bottomAnchor: Double? = null,
var leftAnchor: Double? = null
) {
fun applyToNode(node: T): T {
topAnchor?.let { AnchorPane.setTopAnchor(node, it) }
rightAnchor?.let { AnchorPane.setRightAnchor(node, it) }
bottomAnchor?.let { AnchorPane.setBottomAnchor(node, it) }
leftAnchor?.let { AnchorPane.setLeftAnchor(node, it) }
return node
}
}
abstract class MarginableConstraints {
abstract var margin: Insets
var marginTop: Double
get() = margin.top
set(value) {
margin = margin.let { Insets(value, it.right, it.bottom, it.left) }
}
var marginRight: Double
get() = margin.right
set(value) {
margin = margin.let { Insets(it.top, value, it.bottom, it.left) }
}
var marginBottom: Double
get() = margin.bottom
set(value) {
margin = margin.let { Insets(it.top, it.right, value, it.left) }
}
var marginLeft: Double
get() = margin.left
set(value) {
margin = margin.let { Insets(it.top, it.right, it.bottom, value) }
}
fun marginTopBottom(value: Double) {
marginTop = value
marginBottom = value
}
fun marginLeftRight(value: Double) {
marginLeft = value
marginRight = value
}
}
@Suppress("CAST_NEVER_SUCCEEDS", "UNCHECKED_CAST")
inline fun TableColumn.makeEditable() {
isEditable = true
when (S::class.javaPrimitiveType ?: S::class) {
Number::class -> setCellFactory(TextFieldTableCell.forTableColumn(NumberStringConverter() as StringConverter))
String::class -> setCellFactory(TextFieldTableCell.forTableColumn(DefaultStringConverter() as StringConverter))
LocalDate::class -> setCellFactory(TextFieldTableCell.forTableColumn(LocalDateStringConverter() as StringConverter))
LocalTime::class -> setCellFactory(TextFieldTableCell.forTableColumn(LocalTimeStringConverter() as StringConverter))
LocalDateTime::class -> setCellFactory(TextFieldTableCell.forTableColumn(LocalDateTimeStringConverter() as StringConverter))
Boolean::class.javaPrimitiveType -> {
this as TableColumn
setCellFactory(CheckBoxTableCell.forTableColumn(this))
}
else -> throw RuntimeException("makeEditable() is not implemented for specified class type:" + S::class.qualifiedName)
}
}
fun TreeTableView.populate(itemFactory: (T) -> TreeItem = { TreeItem(it) }, childFactory: (TreeItem) -> Iterable?) =
populateTree(root, itemFactory, childFactory)
fun TreeView.populate(itemFactory: (T) -> TreeItem = { TreeItem(it) }, childFactory: (TreeItem) -> Iterable?) =
populateTree(root, itemFactory, childFactory)
/**
* Add children to the given item by invoking the supplied childFactory function, which converts
* a TreeItem<T> to a List<T>?.
*
* If the childFactory returns a non-empty list, each entry in the list is converted to a TreeItem<T>
* via the supplied itemProcessor function. The default itemProcessor from TreeTableView.populate and TreeTable.populate
* simply wraps the given T in a TreeItem, but you can override it to add icons etc. Lastly, the populateTree
* function is called for each of the generated child items.
*/
fun populateTree(item: TreeItem, itemFactory: (T) -> TreeItem, childFactory: (TreeItem) -> Iterable?) {
childFactory.invoke(item)?.map { itemFactory.invoke(it) }?.apply {
item.children.setAll(this)
forEach { populateTree(it, itemFactory, childFactory) }
}
}
/**
* Return the UIComponent (View or Fragment) that owns this Parent
*/
inline fun Parent.uiComponent(): T? = properties[UI_COMPONENT_PROPERTY] as? T
/**
* Find all UIComponents of the specified type that owns any of this node's children
*/
inline fun Parent.findAll(): List = childrenUnmodifiable
.filterIsInstance()
.filter { it.uiComponent() is T }
.map { it.uiComponent()!! }
/**
* Find all UIComponents of the specified type that owns any of this UIComponent's root node's children
*/
inline fun UIComponent.findAll(): List = root.findAll()
/**
* Find the first UIComponent of the specified type that owns any of this node's children
*/
inline fun Parent.lookup(noinline op: (T.() -> Unit)? = null): T? {
val result = findAll().getOrNull(0)
if (result != null) op?.invoke(result)
return result
}
/**
* Find the first UIComponent of the specified type that owns any of this UIComponent's root node's children
*/
inline fun UIComponent.lookup(noinline op: (T.() -> Unit)? = null): T? {
val result = findAll().getOrNull(0)
if (result != null) op?.invoke(result)
return result
}
fun EventTarget.removeFromParent() {
if (this is UIComponent) {
root.removeFromParent()
} else if (this is Tab) {
tabPane?.tabs?.remove(this)
} else if (this is Node) {
parent?.getChildList()?.remove(this)
}
}
/**
* Listen for changes to an observable value and replace all content in this Node with the
* new content created by the onChangeBuilder. The builder operates on the node and receives
* the new value of the observable as it's only parameter.
*
* The onChangeBuilder is run immediately with the current value of the property.
*/
fun S.dynamicContent(property: ObservableValue, onChangeBuilder: S.(T?) -> Unit) {
val onChange: (T?) -> Unit = {
getChildList()?.clear()
onChangeBuilder(this@dynamicContent, it)
}
property.onChange(onChange)
onChange(property.value)
}
const val TRANSITIONING_PROPERTY = "tornadofx.transitioning"
/**
* Whether this node is currently being used in a [ViewTransition]. Used to determine whether it can be used in a
* transition. (Nodes can only exist once in the scenegraph, so it cannot be in two transitions at once.)
*/
internal var Node.isTransitioning: Boolean
get() {
val x = properties[TRANSITIONING_PROPERTY]
return x != null && (x !is Boolean || x != false)
}
set(value) {
properties[TRANSITIONING_PROPERTY] = value
}
/**
* Replace this [Node] with another, optionally using a transition animation.
*
* @param replacement The node that will replace this one
* @param transition The [ViewTransition] used to animate the transition
* @return Whether or not the transition will run
*/
fun Node.replaceWith(replacement: Node, transition: ViewTransition? = null, onTransit: (() -> Unit)? = null): Boolean {
if (isTransitioning || replacement.isTransitioning) {
return false
}
onTransit?.invoke()
if (this == scene?.root) {
val scene = scene!!
if (replacement !is Parent) {
throw IllegalArgumentException("Replacement scene root must be a Parent")
}
if (transition != null) {
transition.call(this, replacement) {
scene.root = it as Parent
}
} else {
removeFromParent()
replacement.removeFromParent()
scene.root = replacement
}
return true
} else if (parent is Pane) {
val parent = parent as Pane
val attach = if (parent is BorderPane) {
when (this) {
parent.top -> {
{ it: Node -> parent.top = it }
}
parent.right -> {
{ parent.right = it }
}
parent.bottom -> {
{ parent.bottom = it }
}
parent.left -> {
{ parent.left = it }
}
parent.center -> {
{ parent.center = it }
}
else -> {
{ throw IllegalStateException("Child of BorderPane not found in BorderPane") }
}
}
} else {
val children = parent.children
val index = children.indexOf(this);
{ children.add(index, it) }
}
if (transition != null) {
transition.call(this, replacement, attach)
} else {
removeFromParent()
replacement.removeFromParent()
attach(replacement)
}
return true
} else {
return false
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy