Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
tornadofx.ItemControls.kt Maven / Gradle / Ivy
@file:Suppress("UNCHECKED_CAST")
package tornadofx
import com.sun.javafx.scene.control.skin.TableRowSkin
import javafx.application.Platform
import javafx.beans.InvalidationListener
import javafx.beans.Observable
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
import javafx.beans.binding.ObjectBinding
import javafx.beans.property.*
import javafx.beans.value.ObservableValue
import javafx.beans.value.WritableValue
import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import javafx.event.EventTarget
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.*
import javafx.scene.control.cell.*
import javafx.scene.input.MouseEvent
import javafx.scene.layout.StackPane
import javafx.scene.paint.Color
import javafx.scene.shape.Polygon
import javafx.scene.text.Text
import javafx.util.Callback
import javafx.util.StringConverter
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
/**
* Create a spinner for an arbitrary type. This spinner requires you to configure a value factory, or it will throw an exception.
*/
fun EventTarget.spinner(
editable: Boolean = false,
property: Property? = null,
enableScroll: Boolean = false,
op: Spinner.() -> Unit = {}
) = Spinner().also{
it.isEditable = editable
it.attachTo(this, op)
if (property != null) requireNotNull(it.valueFactory) {
"You must configure the value factory or use the Number based spinner builder " +
"which configures a default value factory along with min, max and initialValue!"
}.valueProperty().apply {
bindBidirectional(property)
ViewModel.register(this, property)
}
if (enableScroll) it.setOnScroll { event ->
if (event.deltaY > 0) it.increment()
if (event.deltaY < 0) it.decrement()
}
if (editable) it.focusedProperty().addListener { _, _, newValue ->
if (!newValue) it.increment(0)
}
}
inline fun EventTarget.spinner(min: T? = null, max: T? = null, initialValue: T? = null, amountToStepBy: T? = null, editable: Boolean = false, property: Property? = null, enableScroll: Boolean = false, noinline op: Spinner.() -> Unit = {}): Spinner {
val spinner: Spinner
val isInt = (property is IntegerProperty && property !is DoubleProperty && property !is FloatProperty) || min is Int || max is Int || initialValue is Int ||
T::class == Int::class || T::class == Integer::class || T::class.javaPrimitiveType == Integer::class.java
if (isInt) {
spinner = Spinner(min?.toInt() ?: 0, max?.toInt() ?: 100, initialValue?.toInt() ?: 0, amountToStepBy?.toInt()
?: 1)
} else {
spinner = Spinner(min?.toDouble() ?: 0.0, max?.toDouble() ?: 100.0, initialValue?.toDouble()
?: 0.0, amountToStepBy?.toDouble() ?: 1.0)
}
if (property != null) {
spinner.valueFactory.valueProperty().bindBidirectional(property)
ViewModel.register(spinner.valueFactory.valueProperty(), property)
}
spinner.isEditable = editable
if (enableScroll) {
spinner.setOnScroll { event ->
if (event.deltaY > 0) spinner.increment()
if (event.deltaY < 0) spinner.decrement()
}
}
if (editable) {
spinner.focusedProperty().addListener { _, _, newValue ->
if (!newValue) {
spinner.increment(0)
}
}
}
return spinner.attachTo(this, op)
}
fun EventTarget.spinner(
items: ObservableList,
editable: Boolean = false,
property: Property? = null,
enableScroll: Boolean = false,
op: Spinner.() -> Unit = {}
) = Spinner(items).attachTo(this,op){
if (property != null) it.valueFactory.valueProperty().apply {
bindBidirectional(property)
ViewModel.register(this, property)
}
it.isEditable = editable
if (enableScroll) it.setOnScroll { event ->
if (event.deltaY > 0) it.increment()
if (event.deltaY < 0) it.decrement()
}
if (editable) it.focusedProperty().addListener { _, _, newValue ->
if (!newValue) it.increment(0)
}
}
fun EventTarget.spinner(
valueFactory: SpinnerValueFactory,
editable: Boolean = false,
property: Property? = null,
enableScroll: Boolean = false,
op: Spinner.() -> Unit = {}
) = Spinner(valueFactory).attachTo(this, op){
if (property != null) it.valueFactory.valueProperty().apply {
bindBidirectional(property)
ViewModel.register(this, property)
}
it.isEditable = editable
if (enableScroll) it.setOnScroll { event ->
if (event.deltaY > 0) it.increment()
if (event.deltaY < 0) it.decrement()
}
if (editable) it.focusedProperty().addListener { _, _, newValue ->
if (!newValue) it.increment(0)
}
}
fun EventTarget.combobox(property: Property? = null, values: List? = null, op: ComboBox.() -> Unit = {}) = ComboBox().attachTo(this, op) {
if (values != null) it.items = values as? ObservableList ?: values.asObservable()
if (property != null) it.bind(property)
}
fun ComboBox.cellFormat(scope: Scope, formatButtonCell: Boolean = true, formatter: ListCell.(T) -> Unit) {
cellFactory = Callback {
//ListView may be defined or not, so properties are set the safe way
SmartListCell(scope, it, mapOf("tornadofx.cellFormat" to formatter))
}
if (formatButtonCell) buttonCell = cellFactory.call(null)
}
fun EventTarget.choicebox(property: Property? = null, values: List? = null, op: ChoiceBox.() -> Unit = {}) = ChoiceBox().attachTo(this, op) {
if (values != null) it.items = (values as? ObservableList) ?: values.asObservable()
if (property != null) it.bind(property)
}
fun EventTarget.listview(values: ObservableList? = null, op: ListView.() -> Unit = {}) = ListView().attachTo(this, op) {
if (values != null) {
if (values is SortedFilteredList) values.bindTo(it)
else it.items = values
}
}
fun EventTarget.listview(values: ReadOnlyListProperty, op: ListView.() -> Unit = {}) = listview(values as ObservableValue>, op)
fun EventTarget.listview(values: ObservableValue>, op: ListView.() -> Unit = {}) = ListView().attachTo(this, op) {
fun rebinder() {
(it.items as? SortedFilteredList)?.bindTo(it)
}
it.itemsProperty().bind(values)
rebinder()
it.itemsProperty().onChange {
rebinder()
}
}
fun EventTarget.tableview(items: ObservableList? = null, op: TableView.() -> Unit = {}) = TableView().attachTo(this, op) {
if (items != null) {
if (items is SortedFilteredList) items.bindTo(it)
else it.items = items
}
}
fun EventTarget.tableview(items: ReadOnlyListProperty, op: TableView.() -> Unit = {}) = tableview(items as ObservableValue>, op)
fun EventTarget.tableview(items: ObservableValue>, op: TableView.() -> Unit = {}) = TableView().attachTo(this, op) {
fun rebinder() {
(it.items as? SortedFilteredList)?.bindTo(it)
}
it.itemsProperty().bind(items)
rebinder()
it.itemsProperty().onChange {
rebinder()
}
items.onChange {
rebinder()
}
}
fun EventTarget.treeview(root: TreeItem? = null, op: TreeView.() -> Unit = {}) = TreeView().attachTo(this, op) {
if (root != null) it.root = root
}
fun EventTarget.treetableview(root: TreeItem? = null, op: TreeTableView.() -> Unit = {}) = TreeTableView().attachTo(this, op) {
if (root != null) it.root = root
}
fun TreeView.lazyPopulate(
leafCheck: (LazyTreeItem) -> Boolean = { !it.hasChildren() },
itemProcessor: (LazyTreeItem) -> Unit = {},
childFactory: (TreeItem) -> List?
) {
fun createItem(value: T) = LazyTreeItem(value, leafCheck, itemProcessor, childFactory).also(itemProcessor)
requireNotNull(root) { "You must set a root TreeItem before calling lazyPopulate" }
task {
childFactory.invoke(root)
} success {
root.children.setAll(it?.map(::createItem) ?: emptyList())
}
}
class LazyTreeItem(
value: T,
val leafCheck: (LazyTreeItem) -> Boolean,
val itemProcessor: (LazyTreeItem) -> Unit = {},
val childFactory: (TreeItem) -> List?
) : TreeItem(value) {
private val leafResult: Boolean by lazy { leafCheck(this) }
var childFactoryInvoked = false
var childFactoryResult: List? = null
override fun isLeaf(): Boolean {
return leafResult
}
override fun getChildren(): ObservableList> {
if (!childFactoryInvoked) {
task {
invokeAndSetChildFactorySynchronously()
} success {
if (childFactoryResult != null) listenForChanges()
}
}
return super.getChildren()
}
private fun listenForChanges() {
(childFactoryResult as? ObservableList)?.addListener(ListChangeListener { change ->
while (change.next()) {
if (change.wasPermutated()) {
children.subList(change.from, change.to).clear()
val permutated = change.list.subList(change.from, change.to).map { newLazyTreeItem(it) }
children.addAll(change.from, permutated)
} else {
if (change.wasRemoved()) {
val removed = change.removed.flatMap { removed -> children.filter { it.value == removed } }
children.removeAll(removed)
}
if (change.wasAdded()) {
val added = change.addedSubList.map { newLazyTreeItem(it) }
children.addAll(change.from, added)
}
}
}
})
}
fun hasChildren(): Boolean = invokeAndSetChildFactorySynchronously().isNullOrEmpty()
private fun invokeAndSetChildFactorySynchronously(): List? {
if (!childFactoryInvoked) {
childFactoryInvoked = true
childFactoryResult = childFactory(this).also { result ->
if(result != null) {
super.getChildren().setAll(result.map { newLazyTreeItem(it) })
}
}
}
return childFactoryResult
}
private fun newLazyTreeItem(item: T) = LazyTreeItem(item, leafCheck, itemProcessor, childFactory).apply { itemProcessor(this) }
}
fun TreeItem.treeitem(value: T? = null, op: TreeItem.() -> Unit = {}): TreeItem {
val treeItem = value?.let { TreeItem(it) } ?: TreeItem()
treeItem.op()
this += treeItem
return treeItem
}
operator fun TreeItem.plusAssign(treeItem: TreeItem) {
this.children.add(treeItem)
}
fun TableView.makeIndexColumn(name: String = "#", startNumber: Int = 1): TableColumn {
return TableColumn(name).apply {
isSortable = false
prefWidth = width
[email protected] += this
setCellValueFactory { ReadOnlyObjectWrapper(items.indexOf(it.value) + startNumber) }
}
}
fun TableColumn.enableTextWrap() = apply {
setCellFactory {
TableCell().apply {
val text = Text()
graphic = text
prefHeight = Control.USE_COMPUTED_SIZE
text.wrappingWidthProperty().bind([email protected] ().subtract(Bindings.multiply(2.0, graphicTextGapProperty())))
text.textProperty().bind(stringBinding(itemProperty()) { get()?.toString() ?: "" })
}
}
}
@Suppress("UNCHECKED_CAST")
fun TableView.addColumnInternal(column: TableColumn, index: Int? = null) {
val columnTarget = properties["tornadofx.columnTarget"] as? ObservableList> ?: columns
if (index == null) columnTarget.add(column) else columnTarget.add(index, column)
}
@Suppress("UNCHECKED_CAST")
fun TreeTableView.addColumnInternal(column: TreeTableColumn, index: Int? = null) {
val columnTarget = properties["tornadofx.columnTarget"] as? ObservableList> ?: columns
if (index == null) columnTarget.add(column) else columnTarget.add(index, column)
}
/**
* Create a column holding children columns
*/
@Suppress("UNCHECKED_CAST")
fun TableView.nestedColumn(title: String, op: TableView.(TableColumn) -> Unit = {}): TableColumn {
val column = TableColumn(title)
addColumnInternal(column)
val previousColumnTarget = properties["tornadofx.columnTarget"] as? ObservableList>
properties["tornadofx.columnTarget"] = column.columns
op(this, column)
properties["tornadofx.columnTarget"] = previousColumnTarget
return column
}
/**
* Create a column holding children columns
*/
@Suppress("UNCHECKED_CAST")
fun TreeTableView.nestedColumn(title: String, op: TreeTableView.() -> Unit = {}): TreeTableColumn {
val column = TreeTableColumn(title)
addColumnInternal(column)
val previousColumnTarget = properties["tornadofx.columnTarget"] as? ObservableList>
properties["tornadofx.columnTarget"] = column.columns
op(this)
properties["tornadofx.columnTarget"] = previousColumnTarget
return column
}
/**
* Create a column using the propertyName of the attribute you want shown.
*/
fun TableView.column(title: String, propertyName: String, op: TableColumn.() -> Unit = {}): TableColumn {
val column = TableColumn(title)
column.cellValueFactory = PropertyValueFactory(propertyName)
addColumnInternal(column)
return column.also(op)
}
/**
* Create a column using the getter of the attribute you want shown.
*/
@JvmName("pojoColumn")
fun TableView.column(title: String, getter: KFunction): TableColumn {
val startIndex = if (getter.name.startsWith("is") && getter.name[2].isUpperCase()) 2 else 3
val propName = getter.name.substring(startIndex).decapitalize()
return this.column(title, propName)
}
/**
* Create a column using the propertyName of the attribute you want shown.
*/
fun TreeTableView.column(title: String, propertyName: String, op: TreeTableColumn.() -> Unit = {}): TreeTableColumn {
val column = TreeTableColumn(title)
column.cellValueFactory = TreeItemPropertyValueFactory(propertyName)
addColumnInternal(column)
return column.also(op)
}
/**
* Create a column using the getter of the attribute you want shown.
*/
@JvmName("pojoColumn")
fun TreeTableView.column(title: String, getter: KFunction): TreeTableColumn {
val startIndex = if (getter.name.startsWith("is") && getter.name[2].isUpperCase()) 2 else 3
val propName = getter.name.substring(startIndex).decapitalize()
return this.column(title, propName)
}
fun TableColumn.useComboBox(items: ObservableList, afterCommit: (TableColumn.CellEditEvent) -> Unit = {}) = apply {
cellFactory = ComboBoxTableCell.forTableColumn(items)
setOnEditCommit {
val property = it.tableColumn.getCellObservableValue(it.rowValue) as Property
property.value = it.newValue
afterCommit(it)
}
}
inline fun TableColumn.useTextField(
converter: StringConverter? = null,
noinline afterCommit: (TableColumn.CellEditEvent) -> Unit = {}
) = apply {
when (T::class) {
String::class -> {
@Suppress("UNCHECKED_CAST")
val stringColumn = this as TableColumn
stringColumn.cellFactory = TextFieldTableCell.forTableColumn()
}
else -> {
requireNotNull(converter) { "You must supply a converter for non String columns" }
cellFactory = TextFieldTableCell.forTableColumn(converter)
}
}
setOnEditCommit {
val property = it.tableColumn.getCellObservableValue(it.rowValue) as Property
property.value = it.newValue
afterCommit(it)
}
}
fun TableColumn.useChoiceBox(items: ObservableList, afterCommit: (TableColumn.CellEditEvent) -> Unit = {}) = apply {
cellFactory = ChoiceBoxTableCell.forTableColumn(items)
setOnEditCommit {
val property = it.tableColumn.getCellObservableValue(it.rowValue) as Property
property.value = it.newValue
afterCommit(it)
}
}
fun TableColumn.useProgressBar(scope: Scope, afterCommit: (TableColumn.CellEditEvent) -> Unit = {}) = apply {
cellFormat(scope) {
addClass(Stylesheet.progressBarTableCell)
graphic = cache {
progressbar(itemProperty().doubleBinding { it?.toDouble() ?: 0.0 }) {
useMaxWidth = true
}
}
}
(this as TableColumn).setOnEditCommit {
val property = it.tableColumn.getCellObservableValue(it.rowValue) as Property
property.value = it.newValue?.toDouble()
afterCommit(it as TableColumn.CellEditEvent)
}
}
fun TableColumn.useCheckbox(editable: Boolean = true) = apply {
cellFormat {
graphic = cache {
alignment = Pos.CENTER
checkbox {
if (editable) {
selectedProperty().bindBidirectional(itemProperty())
setOnAction {
tableView.edit(index, tableColumn)
commitEdit(!isSelected)
}
} else {
selectedProperty().bind(itemProperty())
}
}
}
}
if (editable) {
runLater {
tableView?.isEditable = true
}
}
}
// This was used earlier, but was changed to using cellFormat with cache, see above
//class CheckBoxCell(val makeEditable: Boolean) : TableCell() {
// val checkbox: CheckBox by lazy {
// CheckBox().apply {
// if (makeEditable) {
// selectedProperty().bindBidirectional(itemProperty())
// setOnAction {
// tableView.edit(index, tableColumn)
// commitEdit(!isSelected)
// }
// } else {
// isDisable = true
// selectedProperty().bind(itemProperty())
// }
// }
// }
//
// init {
// if (makeEditable) {
// isEditable = true
// tableView?.isEditable = true
// }
// }
//
// override fun updateItem(item: Boolean?, empty: Boolean) {
// super.updateItem(item, empty)
// style { alignment = Pos.CENTER }
// graphic = if (empty || item == null) null else checkbox
// }
//}
fun ListView.useCheckbox(converter: StringConverter? = null, getter: (S) -> ObservableValue) {
setCellFactory { CheckBoxListCell(getter, converter) }
}
fun TableView.bindSelected(property: Property) {
selectionModel.selectedItemProperty().onChange {
property.value = it
}
}
fun ComboBox.bindSelected(property: Property) {
selectionModel.selectedItemProperty().onChange {
property.value = it
}
}
fun TableView.bindSelected(model: ItemViewModel) {
selectionModel.selectedItemProperty().onChange {
model.item = it
}
}
val TableView.selectedCell: TablePosition?
get() = selectionModel.selectedCells.firstOrNull() as TablePosition?
val TableView.selectedColumn: TableColumn?
get() = selectedCell?.tableColumn
val TableView.selectedValue: Any?
get() = selectedColumn?.getCellObservableValue(selectedItem)?.value
val TreeTableView.selectedCell: TreeTablePosition?
get() = selectionModel.selectedCells.firstOrNull()
val TreeTableView.selectedColumn: TreeTableColumn?
get() = selectedCell?.tableColumn
val TreeTableView.selectedValue: Any?
get() = selectedColumn?.getCellObservableValue(selectionModel.selectedItem)?.value
/**
* Create a column with a value factory that extracts the value from the given mutable
* property and converts the property to an observable value.
*/
inline fun TableView.column(title: String, prop: KMutableProperty1, noinline op: TableColumn.() -> Unit = {}): TableColumn {
val column = TableColumn(title)
column.cellValueFactory = Callback { observable(it.value, prop) }
addColumnInternal(column)
return column.also(op)
}
inline fun TreeTableView.column(title: String, prop: KMutableProperty1, noinline op: TreeTableColumn.() -> Unit = {}): TreeTableColumn {
val column = TreeTableColumn(title)
column.cellValueFactory = Callback { observable(it.value.value, prop) }
addColumnInternal(column)
return column.also(op)
}
/**
* Create a column with a value factory that extracts the value from the given property and
* converts the property to an observable value.
*
* ATTENTION: This function was renamed to `readonlyColumn` to avoid shadowing the version for
* observable properties.
*/
inline fun TableView.readonlyColumn(title: String, prop: KProperty1, noinline op: TableColumn.() -> Unit = {}): TableColumn {
val column = TableColumn(title)
column.cellValueFactory = Callback { observable(it.value, prop) }
addColumnInternal(column)
return column.also(op)
}
inline fun TreeTableView.column(title: String, prop: KProperty1, noinline op: TreeTableColumn.() -> Unit = {}): TreeTableColumn {
val column = TreeTableColumn(title)
column.cellValueFactory = Callback { observable(it.value.value, prop) }
addColumnInternal(column)
return column.also(op)
}
/**
* Create a column with a value factory that extracts the value from the given ObservableValue property.
*/
inline fun TableView.column(title: String, prop: KProperty1>, noinline op: TableColumn.() -> Unit = {}): TableColumn {
val column = TableColumn(title)
column.cellValueFactory = Callback { prop.call(it.value) }
addColumnInternal(column)
return column.also(op)
}
/**
* Add a global edit commit handler to the TableView. You avoid assuming the responsibility
* for writing back the data into your domain object and can consentrate on the actual
* response you want to happen when a column commits and edit.
*/
fun TableView.onEditCommit(onCommit: TableColumn.CellEditEvent.(S) -> Unit) {
fun addEventHandlerForColumn(column: TableColumn) {
column.addEventHandler(TableColumn.editCommitEvent()) { event ->
// Make sure the domain object gets the new value before we notify our handler
Platform.runLater {
onCommit(event, event.rowValue)
}
}
column.columns.forEach(::addEventHandlerForColumn)
}
columns.forEach(::addEventHandlerForColumn)
columns.addListener({ change: ListChangeListener.Change> ->
while (change.next()) {
if (change.wasAdded())
change.addedSubList.forEach(::addEventHandlerForColumn)
}
})
}
/**
* Add a global edit start handler to the TableView. You can use this callback
* to cancel the edit request by calling cancel()
*/
fun TableView.onEditStart(onEditStart: TableColumn.CellEditEvent.(S) -> Unit) {
fun addEventHandlerForColumn(column: TableColumn) {
column.addEventHandler(TableColumn.editStartEvent()) { event ->
onEditStart(event, event.rowValue)
}
column.columns.forEach(::addEventHandlerForColumn)
}
columns.forEach(::addEventHandlerForColumn)
columns.addListener({ change: ListChangeListener.Change> ->
while (change.next()) {
if (change.wasAdded())
change.addedSubList.forEach(::addEventHandlerForColumn)
}
})
}
/**
* Used to cancel an edit event, typically from `onEditStart`
*/
fun TableColumn.CellEditEvent.cancel() {
tableView.edit(-1, tableColumn);
}
/**
* Create a column with a title specified cell type and operate on it. Inside the code block you can call
* `value { it.value.someProperty }` to set up a cellValueFactory that must return T or ObservableValue
*/
@Suppress("UNUSED_PARAMETER")
fun TableView.column(title: String, cellType: KClass, op: TableColumn.() -> Unit = {}): TableColumn {
val column = TableColumn