diff --git a/README.md b/README.md index b2dbcfd6b..5114ed2eb 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ You also need a full rebuild of your code after a version upgrade. If you run in ```bash mvn archetype:generate -DarchetypeGroupId=no.tornado \ -DarchetypeArtifactId=tornadofx-quickstart-archetype \ - -DarchetypeVersion=1.7.17 + -DarchetypeVersion=1.7.20 ``` ### Add TornadoFX to your project @@ -79,14 +79,14 @@ mvn archetype:generate -DarchetypeGroupId=no.tornado \ no.tornado tornadofx - 1.7.17 + 1.7.20 ``` ### Gradle ```groovy -implementation 'no.tornado:tornadofx:1.7.17' +implementation 'no.tornado:tornadofx:1.7.20' ``` ### Snapshots are published to Sonatype diff --git a/pom.xml b/pom.xml index 900d2b607..876777e8d 100644 --- a/pom.xml +++ b/pom.xml @@ -311,7 +311,7 @@ 13 - 1.3.61 + 1.3.72 0.10.0 1.1.2 diff --git a/src/main/java/tornadofx/Collections.kt b/src/main/java/tornadofx/Collections.kt index 13bca1bbb..2543dd671 100644 --- a/src/main/java/tornadofx/Collections.kt +++ b/src/main/java/tornadofx/Collections.kt @@ -5,6 +5,8 @@ package tornadofx import javafx.beans.Observable import javafx.beans.WeakListener import javafx.collections.* +import javafx.collections.transformation.FilteredList +import javafx.collections.transformation.SortedList import tornadofx.FX.IgnoreParentBuilder.No import tornadofx.FX.IgnoreParentBuilder.Once import java.lang.ref.WeakReference @@ -49,7 +51,7 @@ fun observableListOf(collection: Collection): ObservableList = FXColle /** * Returns an empty new [ObservableList] with the given [extractor]. This list reports element updates. */ -fun observableListOf(extractor: (T)->Array): ObservableList = FXCollections.observableArrayList(extractor) +fun observableListOf(extractor: (T) -> Array): ObservableList = FXCollections.observableArrayList(extractor) /** * Returns an empty new [ObservableSet] @@ -372,15 +374,34 @@ fun MutableList.bind(sourceList: Observable } } val listener = ListConversionListener(this, ignoringParentConverter) - (this as? ObservableList)?.setAll(sourceList.map(ignoringParentConverter)) ?: run { + (this as? ObservableList)?.setAll(sourceList.map(ignoringParentConverter)) ?: run { clear() addAll(sourceList.map(ignoringParentConverter)) } sourceList.removeListener(listener) sourceList.addListener(listener) + + when (sourceList) { + is FilteredList -> sourceList.source.addListener(invalidateList(sourceList)) + is SortedList -> sourceList.source.addListener(invalidateList(sourceList)) + is SortedFilteredList -> sourceList.sortedItems.source.addListener(invalidateList(sourceList)) + } + return listener } +fun invalidateList(sourceList: ObservableList): ListChangeListener = object : ListChangeListener, WeakListener { + private val ref: WeakReference> = WeakReference(sourceList) + + override fun onChanged(change: ListChangeListener.Change) { + val list = ref.get() + if (list == null) change.list.removeListener(this) + else while (change.next()) list.invalidate() + } + + override fun wasGarbageCollected() = ref.get() == null +} + /** * Bind this list to the given observable list by converting them into the correct type via the given converter. * Changes to the observable list are synced. @@ -425,13 +446,13 @@ fun MutableList.bind( val listener = MapConversionListener(this, ignoringParentConverter) if (this is ObservableList<*>) { sourceMap.forEach { source -> - val converted = ignoringParentConverter(source.key,source.value) - listener.sourceToTarget[source] = converted + val converted = ignoringParentConverter(source.key, source.value) + listener.sourceToTarget[source.key] = converted } (this as ObservableList).setAll(listener.sourceToTarget.values) } else { clear() - addAll(sourceMap.map{ignoringParentConverter(it.key, it.value) }) + addAll(sourceMap.map { ignoringParentConverter(it.key, it.value) }) } sourceMap.removeListener(listener) sourceMap.addListener(listener) @@ -494,19 +515,23 @@ class MapConversionListener( targetList: MutableList, val converter: (SourceTypeKey, SourceTypeValue) -> TargetType ) : MapChangeListener, WeakListener { - internal val targetRef: WeakReference> = WeakReference(targetList) - internal val sourceToTarget = HashMap, TargetType>() + internal val sourceToTarget = HashMap() + override fun onChanged(change: MapChangeListener.Change) { val list = targetRef.get() if (list == null) { change.map.removeListener(this) + sourceToTarget.clear() } else { if (change.wasRemoved()) { - list.remove(converter(change.key, change.valueRemoved)) + list.remove(sourceToTarget[change.key]) + sourceToTarget.remove(change.key) } if (change.wasAdded()) { - list.add(converter(change.key, change.valueAdded)) + val converted = converter(change.key, change.valueAdded) + sourceToTarget[change.key] = converted + list.add(converted) } } } @@ -576,8 +601,16 @@ class SetConversionListener(targetList: MutableList ObservableList.invalidate() { - if (isNotEmpty()) this[0] = this[0] + // bug compiler: Error:(606, 31) Kotlin: Cannot access 'predicate': it is private in 'FilteredList' + @Suppress("UsePropertyAccessSyntax") + if (isNotEmpty()) when (this) { + is FilteredList -> setPredicate(getPredicate()) + is SortedList -> setComparator(getComparator()) + // invalidate FilteredList ? + is SortedFilteredList -> sortedItems.comparator = sortedItems.comparator + else -> this[0] = this[0] + } } @Deprecated("Use `observableListOf()` instead.", ReplaceWith("observableListOf(entries)", "tornadofx.observableListOf")) -fun observableList(vararg entries: T) : ObservableList = FXCollections.observableArrayList(entries.toList()) +fun observableList(vararg entries: T): ObservableList = FXCollections.observableArrayList(entries.toList()) diff --git a/src/main/java/tornadofx/Component.kt b/src/main/java/tornadofx/Component.kt index bc19b1f2a..5466dc7a4 100644 --- a/src/main/java/tornadofx/Component.kt +++ b/src/main/java/tornadofx/Component.kt @@ -6,6 +6,7 @@ import javafx.application.HostServices import javafx.beans.binding.BooleanExpression import javafx.beans.property.* import javafx.beans.value.ChangeListener +import javafx.beans.value.ObservableValue import javafx.collections.FXCollections import javafx.concurrent.Task import javafx.event.EventDispatchChain @@ -1318,6 +1319,100 @@ fun U.whenUndockedOnce(listener: (U) -> Unit) { whenUndocked(wrapped) } +/** + * 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): T = apply { + root.managedWhen(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): T = apply { + root.visibleWhen(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 { + root.hiddenWhen(predicate) +} + +/** + * 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 { + root.disableWhen(predicate) +} + +/** + * This extension function will make sure that the given UI component 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 UI component is enabled when ever, + * the given [predicate] observable boolean value equals true. + */ +fun T.enableWhen(predicate: ObservableValue) = apply { + root.enableWhen(predicate) +} + +/** + * This extension function will make sure that the given UI component will only be visible in the scene graph, + * if the given [expr] returning an observable boolean value equals true. + */ +fun T.removeWhen(predicate: ObservableValue) = root.removeWhen(predicate) + +/** + * This extension function will make sure that the given UI component will only be visible in the scene graph, + * if the given [predicate] observable boolean value equals true. + */ +fun T.removeWhen(expr: () -> ObservableValue): T = apply { root.removeWhen(expr()) } + abstract class Fragment @JvmOverloads constructor(title: String? = null, icon: Node? = null) : UIComponent(title, icon) abstract class View @JvmOverloads constructor(title: String? = null, icon: Node? = null) : UIComponent(title, icon), ScopedInstance diff --git a/src/main/java/tornadofx/FX.kt b/src/main/java/tornadofx/FX.kt index fc8db1b5a..a74583e18 100644 --- a/src/main/java/tornadofx/FX.kt +++ b/src/main/java/tornadofx/FX.kt @@ -679,12 +679,9 @@ fun EventTarget.getChildList(): MutableList? = when (this) { @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") private fun Parent.getChildrenReflectively(): MutableList? { - val getter = this.javaClass.findMethodByName("getChildren") - if (getter != null && java.util.List::class.java.isAssignableFrom(getter.returnType)) { - getter.isAccessible = true - return getter.invoke(this) as MutableList - } - return null + return javaClass.findMethodByName("getChildren") + ?.takeIf { java.util.List::class.java.isAssignableFrom(it.returnType) && canOrSetAccessMethod(it) } + ?.invoke(this) as MutableList } var Window.aboutToBeShown: Boolean diff --git a/src/main/java/tornadofx/ItemControls.kt b/src/main/java/tornadofx/ItemControls.kt index d798342ea..74619c326 100644 --- a/src/main/java/tornadofx/ItemControls.kt +++ b/src/main/java/tornadofx/ItemControls.kt @@ -860,7 +860,7 @@ fun TableView.selectOnDrag() { var startColumn = columns.first() // Record start position and clear selection unless Control is down - addEventFilter(MouseEvent.MOUSE_PRESSED) { + addEventHandler(MouseEvent.MOUSE_PRESSED) { startRow = 0 (it.pickResult.intersectedNode as? TableCell<*, *>)?.apply { @@ -876,7 +876,7 @@ fun TableView.selectOnDrag() { } // Select items while dragging - addEventFilter(MouseEvent.MOUSE_DRAGGED) { + addEventHandler(MouseEvent.MOUSE_DRAGGED) { (it.pickResult.intersectedNode as? TableCell<*, *>)?.apply { if (items.size > index) { if (selectionModel.isCellSelectionEnabled) { @@ -936,12 +936,12 @@ class TableViewEditModel(val tableView: TableView) { // Add columns and track changes to columns tableView.columns.forEach(::addEventHandlerForColumn) - tableView.columns.addListener({ change: ListChangeListener.Change> -> + 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 { diff --git a/src/main/java/tornadofx/Json.kt b/src/main/java/tornadofx/Json.kt index e4b9e5798..562666c1d 100644 --- a/src/main/java/tornadofx/Json.kt +++ b/src/main/java/tornadofx/Json.kt @@ -126,7 +126,7 @@ fun JsonObject.getDouble(vararg key: String): Double = double(*key)!! fun JsonObject.jsonNumber(vararg key: String): JsonNumber? = firstNonNull(*key) { getJsonNumber(it) } fun JsonObject.getJsonNumber(vararg key: String): JsonNumber = jsonNumber(*key)!! -fun JsonObject.float(vararg key: String): Float? = firstNonNull(*key) { getFloat(it) } +fun JsonObject.float(vararg key: String): Float? = firstNonNull(*key) { double(it) }?.toFloat() fun JsonObject.getFloat(vararg key: String): Float = float(*key)!! fun JsonObject.bigdecimal(vararg key: String): BigDecimal? = jsonNumber(*key)?.bigDecimalValue() diff --git a/src/main/java/tornadofx/Lib.kt b/src/main/java/tornadofx/Lib.kt index b9ca99bb3..08c02e618 100644 --- a/src/main/java/tornadofx/Lib.kt +++ b/src/main/java/tornadofx/Lib.kt @@ -15,6 +15,7 @@ import javafx.scene.input.DataFormat import javafx.scene.media.Media import javafx.scene.media.MediaPlayer import java.io.File +import java.lang.reflect.Method import java.util.function.Predicate /** @@ -438,4 +439,13 @@ fun > Sequence.mapEachTo(destination: C, ac */ fun > Array.mapEachTo(destination: C, action: T.() -> R) = mapTo(destination, action) -fun Media.play() = MediaPlayer(this).play() \ No newline at end of file +fun Media.play() = MediaPlayer(this).play() + + +/** + * Checks if a method is available for this, or tries to set access + * + * @return true if access for this this is or has been set + */ +// Java 9+ +internal fun Any.canOrSetAccessMethod(method: Method): Boolean = method.canAccess(this) || method.trySetAccessible() \ No newline at end of file diff --git a/src/main/java/tornadofx/Nodes.kt b/src/main/java/tornadofx/Nodes.kt index 868023ee0..a85a1a29a 100644 --- a/src/main/java/tornadofx/Nodes.kt +++ b/src/main/java/tornadofx/Nodes.kt @@ -925,10 +925,11 @@ fun Node.replaceWith( return true } else if (parent is Pane) { val parent = parent as Pane - val attach = if (parent is BorderPane) { + val attach: (Node) -> Unit = if (parent is BorderPane) { when (this) { parent.top -> { - { it: Node -> parent.top = it } + @Suppress("RedundantLambdaArrow") // bug compiler + { it: Node -> parent.top = it } } parent.right -> { { parent.right = it } @@ -943,7 +944,7 @@ fun Node.replaceWith( { parent.center = it } } else -> { - { throw IllegalStateException("Child of BorderPane not found in BorderPane") } + { it: Node -> throw IllegalStateException("Child of BorderPane not found in BorderPane") } } } } else { diff --git a/src/main/java/tornadofx/skin/tablerow/DirtyDecoratingTableRowSkin.java b/src/main/java/tornadofx/skin/tablerow/DirtyDecoratingTableRowSkin.java index 426e4f647..9031fa9b8 100644 --- a/src/main/java/tornadofx/skin/tablerow/DirtyDecoratingTableRowSkin.java +++ b/src/main/java/tornadofx/skin/tablerow/DirtyDecoratingTableRowSkin.java @@ -9,6 +9,8 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; import tornadofx.TableViewEditModel; +import java.util.ArrayList; +import java.util.List; // This needs to be a Java class because of a Kotlin Bug: // https://youtrack.jetbrains.com/issue/KT-12255 @@ -23,8 +25,8 @@ public DirtyDecoratingTableRowSkin(TableRow tableRow, TableViewEditModel e this.editModel = editModel; } - private Polygon getPolygon(TableCell cell) { - ObservableMap properties = cell.getProperties(); + private Polygon getPolygon(TableCell cell) { + ObservableMap properties = cell.getProperties(); return (Polygon) properties.computeIfAbsent(KEY, x -> { Polygon polygon = new Polygon(0.0, 0.0, 0.0, 10.0, 10.0, 0.0); polygon.setFill(Color.BLUE); @@ -34,10 +36,16 @@ private Polygon getPolygon(TableCell cell) { protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); - final ObservableList children = getChildren(); + final var children = getChildren(); + final var sizeMaxRowElement = children.size(); + List addedChild = null; + List removedChild = null; - children.forEach(child -> { - TableCell cell = (TableCell) child; + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if (!(child instanceof TableCell)) continue; + @SuppressWarnings("unchecked") + TableCell cell = (TableCell) child; T item = null; if (cell.getIndex() > -1 && cell.getTableView().getItems().size() > cell.getIndex()) { item = cell.getTableView().getItems().get(cell.getIndex()); @@ -46,13 +54,23 @@ protected void layoutChildren(double x, double y, double w, double h) { boolean isDirty = item != null && editModel.getDirtyState(item).isDirtyColumn(cell.getTableColumn()); if (isDirty) { if (!children.contains(polygon)) { - children.add(polygon); + if (addedChild == null) addedChild = new ArrayList<>(sizeMaxRowElement); + addedChild.add(polygon); } polygon.relocate(cell.getLayoutX(), y); } else { - children.remove(polygon); + if (removedChild == null) removedChild = new ArrayList<>(sizeMaxRowElement); + removedChild.add(polygon); } - }); + } + + + if (addedChild != null) { + children.addAll(addedChild); + } + if (removedChild != null) { + children.removeAll(removedChild); + } } } diff --git a/src/main/resources/tornadofx/i18n/ViewModel_ru_UA.properties b/src/main/resources/tornadofx/i18n/ViewModel_ru_UA.properties new file mode 100644 index 000000000..6726a9f8b --- /dev/null +++ b/src/main/resources/tornadofx/i18n/ViewModel_ru_UA.properties @@ -0,0 +1 @@ +required=\u042D\u0442\u043E \u043F\u043E\u043B\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \ No newline at end of file diff --git a/src/main/resources/tornadofx/i18n/ViewModel_uk_UA.properties b/src/main/resources/tornadofx/i18n/ViewModel_uk_UA.properties new file mode 100644 index 000000000..d49e72574 --- /dev/null +++ b/src/main/resources/tornadofx/i18n/ViewModel_uk_UA.properties @@ -0,0 +1 @@ +required=\u0426\u0435\u0020\u043f\u043e\u043b\u0435\u0020\u0454\u0020\u043e\u0431\u043e\u0432\u0027\u044f\u0437\u043a\u043e\u0432\u0438\u043c \ No newline at end of file diff --git a/src/main/resources/tornadofx/i18n/Wizard_ru_UA.properties b/src/main/resources/tornadofx/i18n/Wizard_ru_UA.properties new file mode 100644 index 000000000..a0b2ecc57 --- /dev/null +++ b/src/main/resources/tornadofx/i18n/Wizard_ru_UA.properties @@ -0,0 +1,5 @@ +back=< _\u041D\u0430\u0437\u0430\u0434 +cancel=_\u041E\u0442\u043C\u0435\u043D\u0430 +finish=_\u0413\u043E\u0442\u043E\u0432\u043E +next=_\u0414\u0430\u043B\u0435\u0435 > +steps=\u0428\u0430\u0433\u0438 diff --git a/src/main/resources/tornadofx/i18n/Wizard_uk_UA.properties b/src/main/resources/tornadofx/i18n/Wizard_uk_UA.properties new file mode 100644 index 000000000..37797707b --- /dev/null +++ b/src/main/resources/tornadofx/i18n/Wizard_uk_UA.properties @@ -0,0 +1,5 @@ +back=< _\u041d\u0430\u0437\u0430\u0434 +cancel=_\u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 +finish=_\u0424\u0456\u043d\u0456\u0448 +next=_\u0414\u0430\u043b\u0456 > +steps=\u041a\u0440\u043e\u043a\u0438 \ No newline at end of file diff --git a/src/test/kotlin/tornadofx/tests/JsonTest.kt b/src/test/kotlin/tornadofx/tests/JsonTest.kt index 491a280cd..8a6d51581 100644 --- a/src/test/kotlin/tornadofx/tests/JsonTest.kt +++ b/src/test/kotlin/tornadofx/tests/JsonTest.kt @@ -27,7 +27,8 @@ class JsonTest { lastName: String? = null, dob: LocalDate? = null, type: Int? = null, - global: Boolean? = null + global: Boolean? = null, + height: Float? = null ): JsonModelAuto { var firstName by property(firstName) @@ -44,6 +45,9 @@ class JsonTest { var global by property(global) fun globalProperty() = getProperty(AutoPerson2::global) + + var height by property(height) + fun heightProperty() = getProperty(AutoPerson2::height) } @@ -97,8 +101,9 @@ class JsonTest { dob = LocalDate.of(1970, 6, 12) type = 42 global = true + height = 2.0f } - val json = """{"dob":"1970-06-12","firstName":"John","global":true,"lastName":"Doe","type":42}""" + val json = """{"dob":"1970-06-12","firstName":"John","global":true,"height":2.0,"lastName":"Doe","type":42}""" Assert.assertEquals(json, p.toJSON().toString()) val l = loadJsonModel(json) Assert.assertEquals("John", l.firstName) @@ -106,6 +111,7 @@ class JsonTest { Assert.assertEquals(LocalDate.of(1970, 6, 12), l.dob) Assert.assertEquals(42, l.type) Assert.assertEquals(true, l.global) + Assert.assertEquals(2.0f, l.height) Assert.assertEquals(json, l.toJSON().toString()) }