
tornadofx.Nodes.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tornadofx Show documentation
Show all versions of tornadofx Show documentation
Lightweight JavaFX Framework for Kotlin
The newest version!
@file:Suppress("UNCHECKED_CAST")
package tornadofx
import com.sun.javafx.scene.control.skin.TableColumnHeader
import javafx.animation.Animation
import javafx.animation.PauseTransition
import javafx.application.Platform
import javafx.beans.binding.BooleanBinding
import javafx.beans.property.DoubleProperty
import javafx.beans.property.ListProperty
import javafx.beans.property.Property
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections.observableArrayList
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.event.EventHandler
import javafx.event.EventTarget
import javafx.geometry.*
import javafx.scene.*
import javafx.scene.control.*
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.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.stage.Modality
import javafx.stage.Stage
import javafx.util.Callback
import javafx.util.Duration
import javafx.util.StringConverter
import javafx.util.converter.*
import tornadofx.osgi.OSGIConsole
import java.math.BigDecimal
import java.math.BigInteger
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.*
import java.util.function.UnaryOperator
import kotlin.reflect.KClass
import kotlin.reflect.full.safeCast
fun EventTarget.getToggleGroup(): ToggleGroup? = properties["tornadofx.togglegroup"] as ToggleGroup?
fun Node.tooltip(text: String? = null, graphic: Node? = null, op: Tooltip.() -> Unit = {}): Tooltip {
val newToolTip = Tooltip(text)
graphic?.apply { newToolTip.graphic = this }
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.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)
}
internal fun Scene.reloadViews() {
if (properties["tornadofx.layoutdebugger"] == null) {
findUIComponents().forEach {
FX.replaceComponent(it)
}
}
}
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().withEach { clearViews() }
} else {
childrenUnmodifiable.asSequence().filterIsInstance().withEach { findUIComponents(list) }
}
}
private fun Parent.clearViews() {
val uicmp = uiComponent()
if (uicmp is View) {
FX.getComponents(uicmp.scope).remove(uicmp.javaClass.kotlin)
} else {
childrenUnmodifiable.asSequence().filterIsInstance().forEach(Parent::clearViews)
}
}
fun Stage.reloadStylesheetsOnFocus() {
if (properties["tornadofx.reloadStylesheetsListener"] == null) {
focusedProperty().onChange { focused ->
if (focused && FX.initialized.value) scene?.reloadStylesheets()
}
properties["tornadofx.reloadStylesheetsListener"] = true
}
}
fun Stage.hookGlobalShortcuts() {
addEventFilter(KeyEvent.KEY_PRESSED, stageGlobalShortcuts)
}
fun Stage.unhookGlobalShortcuts() {
removeEventFilter(KeyEvent.KEY_PRESSED, stageGlobalShortcuts)
}
val Stage.stageGlobalShortcuts: EventHandler
get() {
val key = "tornadofx.stageGlobalShortcuts"
if (properties[key] == null) {
properties[key] = EventHandler {
if (FX.layoutDebuggerShortcut?.match(it) ?: false)
LayoutDebugger.debug(scene)
else if (FX.osgiDebuggerShortcut?.match(it) ?: false && FX.osgiAvailable)
find().openModal(modality = Modality.NONE)
}
}
return properties[key] as EventHandler
}
fun Stage.reloadViewsOnFocus() {
if (properties["tornadofx.reloadViewsListener"] == null) {
focusedProperty().onChange { focused ->
if (focused && FX.initialized.value) scene?.reloadViews()
}
properties["tornadofx.reloadViewsListener"] = true
}
}
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.mapEach { root }.toTypedArray()))
fun EventTarget.replaceChildren(vararg node: Node) {
val children = requireNotNull(getChildList()) { "This node doesn't have a child list" }
children.clear()
children.addAll(node)
}
operator fun EventTarget.plusAssign(node: Node) {
addChildIfPossible(node)
}
fun Pane.clear() {
children.clear()
}
fun T.replaceChildren(op: T.() -> Unit) {
getChildList()?.clear()
op(this)
}
fun Node.wrapIn(wrapper: Parent) {
parent?.replaceWith(wrapper)
wrapper.addChildIfPossible(this)
}
fun EventTarget.add(node: Node) = plusAssign(node)
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(Region.USE_PREF_SIZE) else Unit
var Region.usePrefHeight: Boolean
get() = height == prefHeight
set(value) = if (value) setMinHeight(Region.USE_PREF_SIZE) else Unit
var Region.usePrefSize: Boolean
get() = maxWidth == Double.MAX_VALUE && maxHeight == Double.MAX_VALUE
set(value) = if (value) setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE) else Unit
fun point(x: Number, y: Number) = Point2D(x.toDouble(), y.toDouble())
fun point(x: Number, y: Number, z: Number) = Point3D(x.toDouble(), y.toDouble(), z.toDouble())
infix fun Number.xy(y: Number) = Point2D(toDouble(), y.toDouble())
fun TableView.resizeColumnsToFitContent(resizeColumns: List> = contentColumns, maxRows: Int = 50, afterResize: () -> Unit = {}) {
val doResize = {
try {
val resizer = skin.javaClass.getDeclaredMethod("resizeColumnToFitContent", TableColumn::class.java, Int::class.java)
resizer.isAccessible = true
resizeColumns.forEach {
if (it.isVisible)
try {
resizer(skin, it, maxRows)
} catch (ignored: Exception) {
}
}
afterResize()
} catch (ex: Throwable) {
// Silent for now, it is usually run multiple times
// log.warning("Unable to resize columns to content: ${columns.joinToString{ it.text }}")
}
}
if (skin == null) skinProperty().onChangeOnce { doResize() } else doResize()
}
fun TreeTableView.resizeColumnsToFitContent(resizeColumns: List> = contentColumns, maxRows: Int = 50, afterResize: () -> Unit = {}) {
val doResize = {
try {
val resizer = skin.javaClass.getDeclaredMethod("resizeColumnToFitContent", TreeTableColumn::class.java, Int::class.java)
resizer.isAccessible = true
resizeColumns.forEach {
if (it.isVisible)
try {
resizer.invoke(skin, it, maxRows)
} catch (ignored: Exception) {
}
}
afterResize.invoke()
} catch (ex: Throwable) {
ex.printStackTrace()
// Silent for now, it is usually run multiple times
// log.warning("Unable to resize columns to content: ${columns.joinToString{ it.text }}")
}
}
if (skin == null) skinProperty().onChangeOnce { 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 ListView.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.filter(predicate).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.filter(predicate).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
fun TableView.selectFirst() = selectionModel.selectFirst()
fun TreeTableView.selectFirst() = selectionModel.selectFirst()
val ComboBox.selectedItem: T?
get() = selectionModel.selectedItem
fun TableView.onSelectionChange(func: (S?) -> Unit) =
selectionModel.selectedItemProperty().addListener({ observable, oldValue, newValue -> func(newValue) })
fun TreeTableView.bindSelected(property: Property) {
selectionModel.selectedItemProperty().onChange {
property.value = it?.value
}
}
fun TreeTableView.bindSelected(model: ItemViewModel) = this.bindSelected(model.itemProperty)
class TableColumnCellCache(private val cacheProvider: (T) -> Node) {
private val store = mutableMapOf()
fun getOrCreateNode(value: T) = store.getOrPut(value) { cacheProvider(value) }
}
fun TableColumn.cellDecorator(decorator: TableCell.(T) -> Unit) {
val originalFactory = cellFactory
cellFactory = Callback { column: TableColumn ->
val cell = originalFactory.call(column)
cell.itemProperty().addListener { _, _, newValue ->
if (newValue != null) decorator(cell, newValue)
}
cell
}
}
fun TreeTableColumn.cellFormat(formatter: (TreeTableCell.(T) -> Unit)) {
cellFactory = Callback { column: TreeTableColumn ->
object : TreeTableCell() {
private val defaultStyle = style
// technically defined as TreeTableCell.DEFAULT_STYLE_CLASS = "tree-table-cell", but this is private
private val defaultStyleClass = listOf(*styleClass.toTypedArray())
override fun updateItem(item: T, empty: Boolean) {
super.updateItem(item, empty)
if (item == null || empty) {
text = null
graphic = null
style = defaultStyle
styleClass.setAll(defaultStyleClass)
} else {
formatter(this, item)
}
}
}
}
}
enum class EditEventType(val editing: Boolean) {
StartEdit(true), CommitEdit(false), CancelEdit(false)
}
/**
* 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.isInsideRow() && !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!!)
}
}
fun Node.onDoubleClick(action: () -> Unit) {
setOnMouseClicked {
if (it.clickCount == 2)
action()
}
}
fun Node.onLeftClick(clickCount: Int = 1, action: () -> Unit) {
setOnMouseClicked {
if (it.clickCount == clickCount && it.button === MouseButton.PRIMARY)
action()
}
}
fun Node.onRightClick(clickCount: Int = 1, action: () -> Unit) {
setOnMouseClicked {
if (it.clickCount == clickCount && it.button === MouseButton.SECONDARY)
action()
}
}
/**
* 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.isInsideRow() && !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 ListProperty.asyncItems(func: FXTask<*>.() -> Collection) =
task { func(this) } success { value = (it as? ObservableList) ?: observableArrayList(it) }
fun ObservableList.asyncItems(func: FXTask<*>.() -> Collection) =
task { func(this) } success { setAll(it) }
fun SortedFilteredList.asyncItems(func: FXTask<*>.() -> Collection) =
task { func(this) } success { items.setAll(it) }
fun TableView.asyncItems(func: FXTask<*>.() -> Collection) =
task(func = func).success { if (items == null) items = observableArrayList(it) else items.setAll(it) }
fun ComboBox.asyncItems(func: FXTask<*>.() -> Collection) =
task(func = func).success { if (items == null) items = observableArrayList(it) else items.setAll(it) }
fun TableView.onUserDelete(action: (T) -> Unit) {
addEventFilter(KeyEvent.KEY_PRESSED, { event ->
if (event.code == KeyCode.BACK_SPACE && selectedItem != null)
action(selectedItem!!)
})
}
/**
* Did the event occur inside a TableRow, TreeTableRow or ListCell?
*/
fun EventTarget.isInsideRow(): 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<*> || this is ListCell<*>)
return true
if (this.parent != null)
return this.parent.isInsideRow()
return false
}
/**
* Access BorderPane constraints to manipulate and apply on this control
*/
inline fun T.borderpaneConstraints(op: (BorderPaneConstraint.() -> Unit)): T {
val bpc = BorderPaneConstraint(this)
bpc.op()
return bpc.applyToNode(this)
}
class BorderPaneConstraint(node: Node,
override var margin: Insets? = BorderPane.getMargin(node),
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
*/
inline fun T.gridpaneConstraints(op: (GridPaneConstraint.() -> Unit)): T {
val gpc = GridPaneConstraint(this)
gpc.op()
return gpc.applyToNode(this)
}
class GridPaneConstraint(node: Node,
var columnIndex: Int? = null,
var rowIndex: Int? = null,
var hGrow: Priority? = null,
var vGrow: Priority? = null,
override var margin: Insets? = GridPane.getMargin(node),
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
}
}
inline fun T.vboxConstraints(op: (VBoxConstraint.() -> Unit)): T {
val c = VBoxConstraint(this)
c.op()
return c.applyToNode(this)
}
inline fun T.stackpaneConstraints(op: (StackpaneConstraint.() -> Unit)): T {
val c = StackpaneConstraint(this)
c.op()
return c.applyToNode(this)
}
class VBoxConstraint(node: Node,
override var margin: Insets? = VBox.getMargin(node),
var vGrow: Priority? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin?.let { VBox.setMargin(node, it) }
vGrow?.let { VBox.setVgrow(node, it) }
return node
}
}
class StackpaneConstraint(node: Node,
override var margin: Insets? = StackPane.getMargin(node),
var alignment: Pos? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin?.let { StackPane.setMargin(node, it) }
alignment?.let { StackPane.setAlignment(node, it) }
return node
}
}
inline fun T.hboxConstraints(op: (HBoxConstraint.() -> Unit)): T {
val c = HBoxConstraint(this)
c.op()
return c.applyToNode(this)
}
class HBoxConstraint(node: Node,
override var margin: Insets? = HBox.getMargin(node),
var hGrow: Priority? = null
) : MarginableConstraints() {
fun applyToNode(node: T): T {
margin?.let { HBox.setMargin(node, it) }
hGrow?.let { HBox.setHgrow(node, it) }
return node
}
}
var Node.hgrow: Priority?
get() = HBox.getHgrow(this)
set(value) {
HBox.setHgrow(this, value)
}
var Node.vgrow: Priority?
get() = VBox.getVgrow(this)
set(value) {
VBox.setVgrow(this, value)
// Input Container vgrow must propagate to Field and Fieldset
if (parent?.parent is Field) {
VBox.setVgrow(parent.parent, value)
if (parent.parent?.parent is Fieldset)
VBox.setVgrow(parent.parent.parent, value)
}
}
inline fun T.anchorpaneConstraints(op: AnchorPaneConstraint.() -> Unit): T {
val c = AnchorPaneConstraint()
c.op()
return c.applyToNode(this)
}
class AnchorPaneConstraint(
var topAnchor: Number? = null,
var rightAnchor: Number? = null,
var bottomAnchor: Number? = null,
var leftAnchor: Number? = null
) {
fun applyToNode(node: T): T {
topAnchor?.let { AnchorPane.setTopAnchor(node, it.toDouble()) }
rightAnchor?.let { AnchorPane.setRightAnchor(node, it.toDouble()) }
bottomAnchor?.let { AnchorPane.setBottomAnchor(node, it.toDouble()) }
leftAnchor?.let { AnchorPane.setLeftAnchor(node, it.toDouble()) }
return node
}
}
inline fun T.splitpaneConstraints(op: SplitPaneConstraint.() -> Unit): T {
val c = SplitPaneConstraint()
c.op()
return c.applyToNode(this)
}
class SplitPaneConstraint(
var isResizableWithParent: Boolean? = null
) {
fun applyToNode(node: T): T {
isResizableWithParent?.let { SplitPane.setResizableWithParent(node, it) }
return node
}
}
abstract class MarginableConstraints {
abstract var margin: Insets?
var marginTop: Double
get() = margin?.top ?: 0.0
set(value) {
margin = Insets(value, margin?.right ?: 0.0, margin?.bottom ?: 0.0, margin?.left ?: 0.0)
}
var marginRight: Double
get() = margin?.right ?: 0.0
set(value) {
margin = Insets(margin?.top ?: 0.0, value, margin?.bottom ?: 0.0, margin?.left ?: 0.0)
}
var marginBottom: Double
get() = margin?.bottom ?: 0.0
set(value) {
margin = Insets(margin?.top ?: 0.0, margin?.right ?: 0.0, value, margin?.left ?: 0.0)
}
var marginLeft: Double
get() = margin?.left ?: 0.0
set(value) {
margin = Insets(margin?.top ?: 0.0, margin?.right ?: 0.0, margin?.bottom ?: 0.0, 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() = apply {
tableView?.isEditable = true
isEditable = true
when (S::class.javaPrimitiveType ?: S::class) {
Int::class -> cellFactory = TextFieldTableCell.forTableColumn(IntegerStringConverter() as StringConverter)
Integer::class -> cellFactory = TextFieldTableCell.forTableColumn(IntegerStringConverter() as StringConverter)
Integer::class.javaPrimitiveType -> cellFactory = TextFieldTableCell.forTableColumn(IntegerStringConverter() as StringConverter)
Double::class -> cellFactory = TextFieldTableCell.forTableColumn(DoubleStringConverter() as StringConverter)
Double::class.javaPrimitiveType -> cellFactory = TextFieldTableCell.forTableColumn(DoubleStringConverter() as StringConverter)
Float::class -> cellFactory = TextFieldTableCell.forTableColumn(FloatStringConverter() as StringConverter)
Float::class.javaPrimitiveType -> cellFactory = TextFieldTableCell.forTableColumn(FloatStringConverter() as StringConverter)
Long::class -> cellFactory = TextFieldTableCell.forTableColumn(LongStringConverter() as StringConverter)
Long::class.javaPrimitiveType -> cellFactory = TextFieldTableCell.forTableColumn(LongStringConverter() as StringConverter)
Number::class -> cellFactory = TextFieldTableCell.forTableColumn(NumberStringConverter() as StringConverter)
BigDecimal::class -> cellFactory = TextFieldTableCell.forTableColumn(BigDecimalStringConverter() as StringConverter)
BigInteger::class -> cellFactory = TextFieldTableCell.forTableColumn(BigIntegerStringConverter() as StringConverter)
String::class -> cellFactory = TextFieldTableCell.forTableColumn(DefaultStringConverter() as StringConverter)
LocalDate::class -> cellFactory = TextFieldTableCell.forTableColumn(LocalDateStringConverter() as StringConverter)
LocalTime::class -> cellFactory = TextFieldTableCell.forTableColumn(LocalTimeStringConverter() as StringConverter)
LocalDateTime::class -> cellFactory = TextFieldTableCell.forTableColumn(LocalDateTimeStringConverter() as StringConverter)
Boolean::class.javaPrimitiveType -> {
(this as TableColumn).useCheckbox(true)
}
else -> throw RuntimeException("makeEditable() is not implemented for specified class type:" + S::class.qualifiedName)
}
}
fun TableView.regainFocusAfterEdit() = apply {
editingCellProperty().onChange {
if (it == null)
requestFocus()
}
}
fun TableColumn.makeEditable(converter: StringConverter): TableColumn = apply {
tableView?.isEditable = true
cellFactory = TextFieldTableCell.forTableColumn(converter)
}
fun TreeTableView.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?) {
val children = childFactory.invoke(item)
children?.map { itemFactory(it) }?.apply {
item.children.setAll(this)
forEach { populateTree(it, itemFactory, childFactory) }
}
(children as? ObservableList)?.addListener(ListChangeListener { change ->
while (change.next()) {
if (change.wasPermutated()) {
item.children.subList(change.from, change.to).clear()
val permutated = change.list.subList(change.from, change.to).map { itemFactory(it) }
item.children.addAll(change.from, permutated)
permutated.forEach { populateTree(it, itemFactory, childFactory) }
} else {
if (change.wasRemoved()) {
val removed = change.removed.flatMap { removed -> item.children.filter { it.value == removed } }
item.children.removeAll(removed)
}
if (change.wasAdded()) {
val added = change.addedSubList.map { itemFactory(it) }
item.children.addAll(change.from, added)
added.forEach { populateTree(it, itemFactory, childFactory) }
}
}
}
})
}
/**
* Return the UIComponent (View or Fragment) that owns this Parent
*/
inline fun Node.uiComponent(): T? = properties[UI_COMPONENT_PROPERTY] as? T
/**
* Return the UIComponent (View or Fragment) that represents the root of the current Scene within this Stage
*/
inline fun Stage.uiComponent(): T? = scene.root.uiComponent()
/**
* Find all UIComponents of the specified type that owns any of this node's children
*/
inline fun Parent.findAll(): List = childrenUnmodifiable
.filterIsInstance()
.map { it.uiComponent() }
.filterIsInstance()
/**
* 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 = {}): T? = findAll().getOrNull(0)?.also(op)
/**
* 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 = {}): T? = findAll().getOrNull(0)?.also(op)
fun EventTarget.removeFromParent() {
when (this) {
is UIComponent -> root.removeFromParent()
is DrawerItem -> drawer.items.remove(this)
is Tab -> tabPane?.tabs?.remove(this)
is Node -> {
(parent?.parent as? ToolBar)?.items?.remove(this) ?: parent?.getChildList()?.remove(this)
}
is TreeItem<*> -> this.parent.children.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, sizeToScene: Boolean = false, centerOnScreen: Boolean = false, onTransit: () -> Unit = {}): Boolean {
if (isTransitioning || replacement.isTransitioning) {
return false
}
onTransit()
if (this == scene?.root) {
val scene = scene!!
require(replacement is Parent) { "Replacement scene root must be a Parent" }
// Update scene property to support Live Views
replacement.uiComponent()?.properties?.put("tornadofx.scene", scene)
if (transition != null) {
transition.call(this, replacement) {
scene.root = it as Parent
if (sizeToScene) scene.window.sizeToScene()
if (centerOnScreen) scene.window.centerOnScreen()
}
} else {
removeFromParent()
replacement.removeFromParent()
scene.root = replacement
if (sizeToScene) scene.window.sizeToScene()
if (centerOnScreen) scene.window.centerOnScreen()
}
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
}
}
@Deprecated("This will go away in the future. Use the version with centerOnScreen parameter", ReplaceWith("replaceWith(replacement, transition, sizeToScene, false)"))
fun Node.replaceWith(replacement: Node, transition: ViewTransition? = null, sizeToScene: Boolean, onTransit: () -> Unit = {}) =
replaceWith(replacement, transition, sizeToScene, false)
fun Node.hide() {
isVisible = false
isManaged = false
}
fun Node.show() {
isVisible = true
isManaged = true
}
fun Node.whenVisible(runLater: Boolean = true, op: () -> Unit) {
visibleProperty().onChange {
if (it) {
if (runLater) Platform.runLater(op) else op()
}
}
}
inline fun Node.findParent(): T? = findParentOfType(T::class)
@Suppress("UNCHECKED_CAST")
fun Node.findParentOfType(parentType: KClass): T? {
if (parent == null) return null
parentType.safeCast(parent)?.also { return it }
val uicmp = parent.uiComponent()
parentType.safeCast(uicmp)?.also { return it }
return parent?.findParentOfType(parentType)
}
val Region.paddingTopProperty: DoubleProperty
get() = properties.getOrPut("paddingTopProperty") {
proxypropDouble(paddingProperty(), { value.top }) {
Insets(it, value.right, value.bottom, value.left)
}
} as DoubleProperty
val Region.paddingBottomProperty: DoubleProperty
get() = properties.getOrPut("paddingBottomProperty") {
proxypropDouble(paddingProperty(), { value.bottom }) {
Insets(value.top, value.right, it, value.left)
}
} as DoubleProperty
val Region.paddingLeftProperty: DoubleProperty
get() = properties.getOrPut("paddingLeftProperty") {
proxypropDouble(paddingProperty(), { value.left }) {
Insets(value.top, value.right, value.bottom, it)
}
} as DoubleProperty
val Region.paddingRightProperty: DoubleProperty
get() = properties.getOrPut("paddingRightProperty") {
proxypropDouble(paddingProperty(), { value.right }) {
Insets(value.top, it, value.bottom, value.left)
}
} as DoubleProperty
val Region.paddingVerticalProperty: DoubleProperty
get() = properties.getOrPut("paddingVerticalProperty") {
proxypropDouble(paddingProperty(), { paddingVertical.toDouble() }) {
val half = it / 2.0
Insets(half, value.right, half, value.left)
}
} as DoubleProperty
val Region.paddingHorizontalProperty: DoubleProperty
get() = properties.getOrPut("paddingHorizontalProperty") {
proxypropDouble(paddingProperty(), { paddingHorizontal.toDouble() }) {
val half = it / 2.0
Insets(value.top, half, value.bottom, half)
}
} as DoubleProperty
val Region.paddingAllProperty: DoubleProperty
get() = properties.getOrPut("paddingAllProperty") {
proxypropDouble(paddingProperty(), { paddingAll.toDouble() }) {
Insets(it, it, it, it)
}
} as DoubleProperty
// -- Node helpers
/**
* This extension function will automatically bind to the managedProperty of the given node
* and will make sure that it is managed, if the given [expr] returning an observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#managedProperty
*/
fun T.managedWhen(expr: () -> ObservableValue): T = managedWhen(expr())
/**
* This extension function will automatically bind to the managedProperty of the given node
* and will make sure that it is managed, if the given [predicate] an observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#managedProperty)
*/
fun T.managedWhen(predicate: ObservableValue) = apply {
managedProperty().cleanBind(predicate)
}
/**
* This extension function will automatically bind to the visibleProperty of the given node
* and will make sure that it is visible, if the given [predicate] an observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#visibleProperty
*/
fun T.visibleWhen(predicate: ObservableValue) = apply {
visibleProperty().cleanBind(predicate)
}
/**
* This extension function will automatically bind to the visibleProperty of the given node
* and will make sure that it is visible, if the given [expr] returning an observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#visibleProperty
*/
fun T.visibleWhen(expr: () -> ObservableValue): T = visibleWhen(expr())
/**
* This extension function will make sure to hide the given node,
* if the given [expr] returning an observable boolean value equals true.
*/
fun T.hiddenWhen(expr: () -> ObservableValue): T = hiddenWhen(expr())
/**
* This extension function will make sure to hide the given node,
* if the given [predicate] an observable boolean value equals true.
*/
fun T.hiddenWhen(predicate: ObservableValue) = apply {
val binding = if (predicate is BooleanBinding) predicate.not() else predicate.toBinding().not()
visibleProperty().cleanBind(binding)
}
/**
* This extension function will automatically bind to the disableProperty of the given node
* and will disable it, if the given [expr] returning an observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#disable
*/
fun T.disableWhen(expr: () -> ObservableValue): T = disableWhen(expr())
/**
* This extension function will automatically bind to the disableProperty of the given node
* and will disable it, if the given [predicate] observable boolean value equals true.
*
* @see https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#disableProperty
*/
fun T.disableWhen(predicate: ObservableValue) = apply {
disableProperty().cleanBind(predicate)
}
/**
* This extension function will make sure that the given node is enabled when ever,
* the given [expr] returning an observable boolean value equals true.
*/
fun T.enableWhen(expr: () -> ObservableValue): T = enableWhen(expr())
/**
* This extension function will make sure that the given node is enabled when ever,
* the given [predicate] observable boolean value equals true.
*/
fun T.enableWhen(predicate: ObservableValue) = apply {
val binding = if (predicate is BooleanBinding) predicate.not() else predicate.toBinding().not()
disableProperty().cleanBind(binding)
}
/**
* This extension function will make sure that the given node will only be visible in the scene graph,
* if the given [expr] returning an observable boolean value equals true.
*/
fun T.removeWhen(expr: () -> ObservableValue): T = removeWhen(expr())
/**
* This extension function will make sure that the given node will only be visible in the scene graph,
* if the given [predicate] observable boolean value equals true.
*/
fun T.removeWhen(predicate: ObservableValue) = apply {
val remove = booleanBinding(predicate) { predicate.value.not() }
visibleProperty().cleanBind(remove)
managedProperty().cleanBind(remove)
}
fun TextInputControl.editableWhen(predicate: ObservableValue) = apply {
editableProperty().bind(predicate)
}
fun ComboBoxBase<*>.editableWhen(predicate: ObservableValue) = apply {
editableProperty().bind(predicate)
}
fun TableView<*>.editableWhen(predicate: ObservableValue) = apply {
editableProperty().bind(predicate)
}
fun TreeTableView<*>.editableWhen(predicate: ObservableValue) = apply {
editableProperty().bind(predicate)
}
fun ListView<*>.editableWhen(predicate: ObservableValue) = apply {
editableProperty().bind(predicate)
}
/**
* This extension function will make sure that the given [onHover] function will always be calles
* when ever the hoverProperty of the given node changes.
*/
fun T.onHover(onHover: (Boolean) -> Unit) = apply {
hoverProperty().onChange { onHover(isHover) }
}
// -- MenuItem helpers
fun MenuItem.visibleWhen(expr: () -> ObservableValue) = visibleWhen(expr())
fun MenuItem.visibleWhen(predicate: ObservableValue) = visibleProperty().cleanBind(predicate)
fun MenuItem.disableWhen(expr: () -> ObservableValue) = disableWhen(expr())
fun MenuItem.disableWhen(predicate: ObservableValue) = disableProperty().cleanBind(predicate)
fun MenuItem.enableWhen(expr: () -> ObservableValue) = enableWhen(expr())
fun MenuItem.enableWhen(obs: ObservableValue) {
val binding = if (obs is BooleanBinding) obs.not() else obs.toBinding().not()
disableProperty().cleanBind(binding)
}
fun EventTarget.svgicon(shape: String, size: Number = 16, color: Paint = Color.BLACK, op: SVGIcon.() -> Unit = {}) = SVGIcon(shape, size, color).attachTo(this, op)
class SVGIcon(svgShape: String, size: Number = 16, color: Paint = Color.BLACK) : Pane() {
init {
addClass("icon", "svg-icon")
style {
shape = svgShape
backgroundColor += color
minWidth = size.px
minHeight = size.px
maxWidth = size.px
maxHeight = size.px
}
}
}
internal class ShortLongPressHandler(node: Node) {
var holdTimer = PauseTransition(700.millis)
var consume: Boolean = false
lateinit var originatingEvent: MouseEvent
var shortAction: ((MouseEvent) -> Unit)? = null
var longAction: ((MouseEvent) -> Unit)? = null
init {
holdTimer.setOnFinished { longAction?.invoke(originatingEvent) }
node.addEventHandler(MouseEvent.MOUSE_PRESSED) {
originatingEvent = it
holdTimer.playFromStart()
if (consume) it.consume()
}
node.addEventHandler(MouseEvent.MOUSE_RELEASED) {
if (holdTimer.status == Animation.Status.RUNNING) {
holdTimer.stop()
shortAction?.invoke(originatingEvent)
if (consume) it.consume()
}
}
}
}
internal val Node.shortLongPressHandler: ShortLongPressHandler
get() = properties.getOrPut("tornadofx.shortLongPressHandler") {
ShortLongPressHandler(this)
} as ShortLongPressHandler
fun T.shortpress(consume: Boolean = false, action: (InputEvent) -> Unit) = apply {
shortLongPressHandler.apply {
this.consume = consume
this.shortAction = action
}
}
fun T.longpress(threshold: Duration = 700.millis, consume: Boolean = false, action: (MouseEvent) -> Unit) = apply {
shortLongPressHandler.apply {
this.consume = consume
this.holdTimer.duration = threshold
this.longAction = action
}
}
/**
* Create, cache and return a Node and store it within the owning node. Typical usage:
*
*
* ```
* listview(people) {
* cellFormat {
* graphic = cache {
* hbox {
* label("Some large Node graph here")
* }
* }
* }
* }
* ```
*
* Used within a Cell, the cache statement makes sure that the node is only created once per cell during the cell's life time.
* This greatly reduces memory and performance overhead and should be used in every situation where
* a node graph is created and assigned to the graphic property of a cell.
*
* Note that if you call this function without a a unique key parameter, you will only ever create a single
* cached node for this parent. The use case for this function is mostly to cache the graphic node of a cell,
* so for these use cases you don't need to supply a cache key.
*
* Remember that you can still update whatever you assign to graphic below it on each `cellFormat` update item callback.
*
* Important: Make sure to not cache hard coded data from the current item this cell represents, as this will change
* when the cell is reused to display another item. Either bind to the itemProperty with select, or use `cellCache` instead.
*/
fun Node.cache(key: Any = "tornadofx.cachedNode", op: EventTarget.() -> T) = properties.getOrPut(key) {
op(this)
} as T
/**
* Filter the input of the text field by passing each change to the discriminator
* function and only applying the change if the discriminator returns true
*
* To only allow digits for example, do:
*
* filterInput { it.controlNewText.isInt() }
*
* You can also access just the changed text in `it.text` to validate just the new input.
*
*/
fun TextInputControl.filterInput(discriminator: (TextFormatter.Change) -> Boolean) {
textFormatter = TextFormatter(CustomTextFilter(discriminator))
}
/**
* Custom text filter used to supress input values, for example to
* only allow numbers in a textfield. Used via the filterInput {} builder
*/
class CustomTextFilter(private val discriminator: (TextFormatter.Change) -> Boolean) : UnaryOperator {
override fun apply(c: TextFormatter.Change): TextFormatter.Change =
if (discriminator(c)) c else c.clone().apply { text = "" }
}
val Node.indexInParent: Int get() = parent?.childrenUnmodifiable?.indexOf(this) ?: -1
/**
* Create a subscene and attach it to the current container as a child. The root node of the SubScene will be whatever is built inside the `op` builder parameter.
* If no height or width is given, the size property will be bound to it's parent size.
*/
fun EventTarget.subscene(depthBuffer: Boolean = false, antiAlias: SceneAntialiasing = SceneAntialiasing.DISABLED, width: Number? = null, height: Number? = null, op: SubScene.() -> Unit = {}) =
SubScene(StackPane(), width?.toDouble() ?: 0.0, height?.toDouble() ?: 0.0, depthBuffer, antiAlias).apply {
val builderParent = this@subscene as? Region
if (builderParent != null) {
if (width == null)
widthProperty().bind(builderParent.widthProperty())
if (height == null)
heightProperty().bind(builderParent.heightProperty())
}
}.attachTo(this, op)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy