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

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(title)
    addColumnInternal(column)
    return column.also(op)
}

/**
 * Create a column with a value factory that extracts the value from the given callback.
 */
fun  TableView.column(title: String, valueProvider: (TableColumn.CellDataFeatures) -> ObservableValue): TableColumn {
    val column = TableColumn(title)
    column.cellValueFactory = Callback { valueProvider(it) }
    addColumnInternal(column)
    return column
}

/**
 * Configure a cellValueFactory for the column. If the returned value is not observable, it is automatically
 * wrapped in a SimpleObjectProperty for convenience.
 */
@Suppress("UNCHECKED_CAST")
infix fun  TableColumn.value(cellValueFactory: (TableColumn.CellDataFeatures) -> Any?) = apply {
    this.cellValueFactory = Callback {
        val createdValue = cellValueFactory(it as TableColumn.CellDataFeatures)
        (createdValue as? ObservableValue) ?: SimpleObjectProperty(createdValue)
    }
}

@JvmName(name = "columnForObservableProperty")
inline fun  TreeTableView.column(title: String, prop: KProperty1>): TreeTableColumn {
    val column = TreeTableColumn(title)
    column.cellValueFactory = Callback { prop.call(it.value.value) }
    addColumnInternal(column)
    return column
}

/**
 * Create a column with a value factory that extracts the observable value from the given function reference.
 * This method requires that you have kotlin-reflect on your classpath.
 */
inline fun  TableView.column(title: String, observableFn: KFunction>): TableColumn {
    val column = TableColumn(title)
    column.cellValueFactory = Callback { observableFn.call(it.value) }
    addColumnInternal(column)
    return column
}

inline fun  TreeTableView.column(title: String, observableFn: KFunction>): TreeTableColumn {
    val column = TreeTableColumn(title)
    column.cellValueFactory = Callback { observableFn.call(it.value) }
    addColumnInternal(column)
    return column
}

/**
 * Create a column with a value factory that extracts the value from the given callback.
 */
inline fun  TreeTableView.column(title: String, noinline valueProvider: (TreeTableColumn.CellDataFeatures) -> ObservableValue): TreeTableColumn {
    val column = TreeTableColumn(title)
    column.cellValueFactory = Callback { valueProvider(it) }
    addColumnInternal(column)
    return column
}


fun  TableView.rowExpander(expandOnDoubleClick: Boolean = false, expandedNodeCallback: RowExpanderPane.(S) -> Unit): ExpanderColumn {
    val expander = ExpanderColumn(expandedNodeCallback)
    addColumnInternal(expander, 0)
    setRowFactory {
        object : TableRow() {
            override fun createDefaultSkin(): Skin<*> {
                return ExpandableTableRowSkin(this, expander)
            }
        }
    }
    if (expandOnDoubleClick) onUserSelect(2) {
        expander.toggleExpanded(selectionModel.selectedIndex)
    }
    return expander
}

class RowExpanderPane(val tableRow: TableRow<*>, val expanderColumn: ExpanderColumn<*>) : StackPane() {
    init {
        addClass("expander-pane")
    }

    fun toggleExpanded() {
        expanderColumn.toggleExpanded(tableRow.index)
    }

    fun expandedProperty() = expanderColumn.getCellObservableValue(tableRow.index) as SimpleBooleanProperty
    var expanded: Boolean
        get() = expandedProperty().value
        set(value) {
            expandedProperty().value = value
        }

    override fun getUserAgentStylesheet() = RowExpanderPane::class.java.getResource("rowexpanderpane.css").toExternalForm()
}

class ExpanderColumn(private val expandedNodeCallback: RowExpanderPane.(S) -> Unit) : TableColumn() {
    private val expandedNodeCache = HashMap()
    private val expansionState = mutableMapOf()

    init {
        addClass("expander-column")

        cellValueFactory = Callback {
            expansionState.getOrPut(it.value, { getExpandedProperty(it.value) })
        }

        cellFactory = Callback { ToggleCell() }
    }

    fun toggleExpanded(index: Int) {
        val expanded = getCellObservableValue(index) as SimpleBooleanProperty
        expanded.value = !expanded.value
        tableView.refresh()
    }

    fun getOrCreateExpandedNode(tableRow: TableRow): Node? {
        val index = tableRow.index
        if (index in tableView.items.indices) {
            val item = tableView.items[index]!!
            var node: Node? = expandedNodeCache[item]
            if (node == null) {
                node = RowExpanderPane(tableRow, this)
                expandedNodeCallback(node, item)
                expandedNodeCache.put(item, node)
            }
            return node
        }
        return null
    }

    fun getExpandedNode(item: S): Node? = expandedNodeCache[item]

    fun getExpandedProperty(item: S): BooleanProperty {
        var value: BooleanProperty? = expansionState[item]
        if (value == null) {
            value = object : SimpleBooleanProperty(item, "expanded", false) {
                /**
                 * When the expanded state change we refresh the tableview.
                 * If the expanded state changes to false we remove the cached expanded node.
                 */
                override fun invalidated() {
                    tableView.refresh()
                    if (!getValue()) expandedNodeCache.remove(bean)
                }
            }
            expansionState.put(item, value)
        }
        return value
    }

    private inner class ToggleCell : TableCell() {
        private val button = Button()

        init {
            button.isFocusTraversable = false
            button.styleClass.add("expander-button")
            button.setPrefSize(16.0, 16.0)
            button.padding = Insets(0.0)
            button.setOnAction { toggleExpanded(index) }
        }

        override fun updateItem(expanded: Boolean?, empty: Boolean) {
            super.updateItem(expanded, empty)
            if (item == null || empty) {
                graphic = null
            } else {
                button.text = if (expanded == true) "-" else "+"
                graphic = button
            }
        }
    }
}

class ExpandableTableRowSkin(val tableRow: TableRow, val expander: ExpanderColumn) : TableRowSkin(tableRow) {
    var tableRowPrefHeight = -1.0

    init {
        tableRow.itemProperty().addListener { observable, oldValue, newValue ->
            if (oldValue != null) {
                val expandedNode = this.expander.getExpandedNode(oldValue)
                if (expandedNode != null) children.remove(expandedNode)
            }
        }
    }

    val expanded: Boolean
        get() {
            val item = skinnable.item
            return (item != null && expander.getCellData(skinnable.index))
        }

    private fun getContent(): Node? {
        val node = expander.getOrCreateExpandedNode(tableRow)
        if (node !in children) children.add(node)
        return node
    }

    override fun computePrefHeight(width: Double, topInset: Double, rightInset: Double, bottomInset: Double, leftInset: Double): Double {
        tableRowPrefHeight = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
        return if (expanded) tableRowPrefHeight + (getContent()?.prefHeight(width) ?: 0.0) else tableRowPrefHeight
    }

    override fun layoutChildren(x: Double, y: Double, w: Double, h: Double) {
        super.layoutChildren(x, y, w, h)
        if (expanded) getContent()?.resizeRelocate(0.0, tableRowPrefHeight, w, h - tableRowPrefHeight)
    }

}

fun  TableView.enableCellEditing() {
    selectionModel.isCellSelectionEnabled = true
    isEditable = true
}

fun  TableView.selectOnDrag() {
    var startRow = 0
    var startColumn = columns.first()

    // Record start position and clear selection unless Control is down
    addEventFilter(MouseEvent.MOUSE_PRESSED) {
        startRow = 0

        (it.pickResult.intersectedNode as? TableCell<*, *>)?.apply {
            startRow = index
            startColumn = tableColumn as TableColumn?

            if (selectionModel.isCellSelectionEnabled) {
                selectionModel.clearAndSelect(startRow, startColumn)
            } else {
                selectionModel.clearAndSelect(startRow)
            }
        }
    }

    // Select items while dragging
    addEventFilter(MouseEvent.MOUSE_DRAGGED) {
        (it.pickResult.intersectedNode as? TableCell<*, *>)?.apply {
            if (items.size > index) {
                if (selectionModel.isCellSelectionEnabled) {
                    selectionModel.selectRange(startRow, startColumn, index, tableColumn as TableColumn?)
                } else {
                    selectionModel.selectRange(startRow, index)
                }
            }
        }
    }
}

fun  TableView.enableDirtyTracking() = editModel.enableDirtyTracking()

@Suppress("UNCHECKED_CAST")
val  TableView.editModel: TableViewEditModel
    get() = properties.getOrPut("tornadofx.editModel") { TableViewEditModel(this) } as TableViewEditModel

class TableViewEditModel(val tableView: TableView) {
    val items = FXCollections.observableHashMap>()

    val selectedItemDirtyState: ObjectBinding?> by lazy {
        objectBinding(tableView.selectionModel.selectedItemProperty()) { getDirtyState(value) }
    }

    val selectedItemDirty: BooleanBinding by lazy {
        booleanBinding(selectedItemDirtyState) { value?.dirty?.value ?: false }
    }

    fun getDirtyState(item: S): TableColumnDirtyState = items.getOrPut(item) { TableColumnDirtyState(this, item) }

    fun enableDirtyTracking(dirtyDecorator: Boolean = true) {
        if (dirtyDecorator) {
            tableView.setRowFactory {
                object : TableRow() {
                    override fun createDefaultSkin() = DirtyDecoratingTableRowSkin(this, this@TableViewEditModel)
                }
            }
        }

        fun addEventHandlerForColumn(column: TableColumn) {
            column.addEventHandler(TableColumn.editCommitEvent()) { event ->
                // This fires before the column value is changed (else we would use onEditCommit)
                val item = event.rowValue
                val itemTracker = items.getOrPut(item) { TableColumnDirtyState(this, item) }
                val initialValue = itemTracker.dirtyColumns.getOrPut(event.tableColumn) {
                    event.tableColumn.getValue(item)
                }
                if (initialValue == event.newValue) {
                    itemTracker.dirtyColumns.remove(event.tableColumn)
                } else {
                    itemTracker.dirtyColumns[event.tableColumn] = initialValue
                }
                selectedItemDirty.invalidate()
            }
        }

        // Add columns and track changes to columns
        tableView.columns.forEach(::addEventHandlerForColumn)
        tableView.columns.addListener({ change: ListChangeListener.Change> ->
            while (change.next()) {
                if (change.wasAdded())
                    change.addedSubList.forEach(::addEventHandlerForColumn)
            }
        })

        // Remove dirty state for items removed from the TableView
        val listenForRemovals = ListChangeListener {
            while (it.next()) {
                if (it.wasRemoved()) {
                    it.removed.forEach {
                        items.remove(it)
                    }
                }
            }
        }

        // Track removals on current items list
        tableView.items?.addListener(listenForRemovals)

        // Clear items if item list changes and track removals in new list
        tableView.itemsProperty().addListener { observableValue, oldValue, newValue ->
            items.clear()
            oldValue?.removeListener(listenForRemovals)
            newValue?.addListener(listenForRemovals)
        }
    }

    /**
     * Commit the current item, or just the given column for this item if a column is supplied
     */
    fun commit(item: S, column: TableColumn<*, *>? = null) {
        val dirtyState = getDirtyState(item)
        if (column == null) dirtyState.commit() else dirtyState.commit(column)
    }

    fun commit() {
        items.values.forEach { it.commit() }
    }

    fun rollback() {
        items.values.forEach { it.rollback() }
    }

    /**
     * Rollback the current item, or just the given column for this item if a column is supplied
     */
    fun rollback(item: S, column: TableColumn<*, *>? = null) {
        val dirtyState = getDirtyState(item)
        if (column == null) dirtyState.rollback() else dirtyState.rollback(column)
    }

    fun commitSelected() {
        val selected = selectedItemDirtyState.value?.item
        if (selected != null) commit(selected)
    }

    fun rollbackSelected() {
        val selected = selectedItemDirtyState.value?.item
        if (selected != null) rollback(selected)
    }

    fun isDirty(item: S): Boolean = getDirtyState(item).dirty.value
}

class TableColumnDirtyState(val editModel: TableViewEditModel, val item: S) : Observable {
    val invalidationListeners = ArrayList()

    // Dirty columns and initial value
    private var _dirtyColumns: ObservableMap, Any?>? = null
    val dirtyColumns: ObservableMap, Any?>
        get() {
            if (_dirtyColumns == null)
                _dirtyColumns = FXCollections.observableHashMap, Any?>()
            return _dirtyColumns!!
        }

    val dirty: BooleanBinding by lazy { booleanBinding(dirtyColumns) { isNotEmpty() } }
    val isDirty: Boolean get() = dirty.value

    fun getDirtyColumnProperty(column: TableColumn<*, *>) = booleanBinding(dirtyColumns) { containsKey(column as TableColumn) }

    fun isDirtyColumn(column: TableColumn<*, *>) = dirtyColumns.containsKey(column as TableColumn)

    init {
        dirtyColumns.addListener(InvalidationListener {
            invalidationListeners.forEach { it.invalidated(this) }
        })
    }

    override fun removeListener(listener: InvalidationListener) {
        invalidationListeners.remove(listener)
    }

    override fun addListener(listener: InvalidationListener) {
        invalidationListeners.add(listener)
    }

    override fun equals(other: Any?) = other is TableColumnDirtyState<*> && other.item == item
    override fun hashCode() = item?.hashCode() ?: throw IllegalStateException("Item must be present")

    fun rollback(column: TableColumn<*, *>) {
        val initialValue = dirtyColumns[column as TableColumn]
        if (initialValue != null) {
            column.setValue(item, initialValue)
            dirtyColumns.remove(column)
        }
        editModel.tableView.refresh()
    }

    fun commit(column: TableColumn<*, *>) {
        val initialValue = dirtyColumns[column as TableColumn]
        if (initialValue != null) {
            dirtyColumns.remove(column)
        }
        editModel.tableView.refresh()
    }

    fun rollback() {
        dirtyColumns.forEach {
            it.key.setValue(item, it.value)
        }
        dirtyColumns.clear()
        editModel.selectedItemDirtyState.invalidate()
        editModel.tableView.refresh()
    }

    fun commit() {
        dirtyColumns.clear()
        editModel.selectedItemDirtyState.invalidate()
        editModel.tableView.refresh()
    }

}

@Suppress("UNCHECKED_CAST")
class DirtyDecoratingTableRowSkin(tableRow: TableRow, val editModel: TableViewEditModel) : TableRowSkin(tableRow) {
    private fun getPolygon(cell: TableCell) =
            cell.properties.getOrPut("tornadofx.dirtyStatePolygon") { Polygon(0.0, 0.0, 0.0, 10.0, 10.0, 0.0).apply { fill = Color.BLUE } } as Polygon

    override fun layoutChildren(x: Double, y: Double, w: Double, h: Double) {
        super.layoutChildren(x, y, w, h)

        cells.forEach { cell ->
            val item = if (cell.index > -1 && cell.tableView.items.size > cell.index) cell.tableView.items[cell.index] else null
            val polygon = getPolygon(cell)
            val isDirty = item != null && editModel.getDirtyState(item).isDirtyColumn(cell.tableColumn)
            if (isDirty) {
                if (polygon !in children)
                    children.add(polygon)

                polygon.relocate(cell.layoutX, y)
            } else {
                children.remove(polygon)
            }
        }

    }
}

/**
 * Write a value into the property representing this TableColumn, provided
 * the property is writable.
 */
@Suppress("UNCHECKED_CAST")
fun  TableColumn.setValue(item: S, value: T?) {
    val property = getTableColumnProperty(item)
    (property as? WritableValue)?.value = value
}

/**
 * Get the value from the property representing this TableColumn.
 */
fun  TableColumn.getValue(item: S) = getTableColumnProperty(item).value

/**
 * Get the property representing this TableColumn for the given item.
 */
fun  TableColumn.getTableColumnProperty(item: S): ObservableValue {
    val param = TableColumn.CellDataFeatures(tableView, this, item)
    val property = cellValueFactory.call(param)
    return property
}