You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/11/01 17:17:35 UTC
[sis] branch geoapi-4.0 updated: Avoid blocking the event thread
when asking for a label implies a calls to `Resource.getMetadata()`.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new a47a93c Avoid blocking the event thread when asking for a label implies a calls to `Resource.getMetadata()`.
a47a93c is described below
commit a47a93cc4265594f6ffdf311f549316ccf4157da
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Nov 1 18:10:28 2021 +0100
Avoid blocking the event thread when asking for a label implies a calls to `Resource.getMetadata()`.
---
.../apache/sis/gui/dataset/ResourceExplorer.java | 8 +-
.../org/apache/sis/gui/dataset/ResourceTree.java | 341 ++++++++++++---------
.../org/apache/sis/internal/gui/GUIUtilities.java | 19 +-
3 files changed, 227 insertions(+), 141 deletions(-)
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index d0936a4..7156284 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -523,7 +523,13 @@ public class ResourceExplorer extends WindowManager {
} else {
return null;
}
- return new SelectedData(resources.getTitle(resource, false), table, grid, localized());
+ String text;
+ try {
+ text = ResourceTree.findLabel(resource, resources.locale);
+ } catch (DataStoreException | RuntimeException e) {
+ text = Vocabulary.getResources(resources.locale).getString(Vocabulary.Keys.Unnamed);
+ }
+ return new SelectedData(text, table, grid, localized());
}
/**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
index f471e33..f79ab40 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
@@ -26,7 +26,6 @@ import java.util.Locale;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.Optional;
import javafx.application.Platform;
import javafx.concurrent.Task;
@@ -64,6 +63,7 @@ import org.apache.sis.internal.storage.io.IOUtilities;
import org.apache.sis.internal.gui.ResourceLoader;
import org.apache.sis.internal.gui.BackgroundThreads;
import org.apache.sis.internal.gui.ExceptionReporter;
+import org.apache.sis.internal.gui.GUIUtilities;
import org.apache.sis.internal.gui.LogHandler;
import org.apache.sis.internal.gui.Resources;
import org.apache.sis.internal.gui.Styles;
@@ -90,12 +90,9 @@ import org.apache.sis.util.logging.Logging;
* by another {@link ResourceTree} instance.</li>
* </ul>
*
- * @todo Listen to warnings and save log records in a separated collection for each data store.
- * Add to the contextual menu an option for viewing the log records of selected data store.
- *
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -137,14 +134,19 @@ public class ResourceTree extends TreeView<Resource> {
}
/**
- * Sets the root {@link Resource} of this tree. The root resource is typically,
- * but not necessarily, a {@link DataStore} instance. If other resources existed
- * before this method call, they are discarded.
+ * Sets the root {@link Resource} of this tree.
+ * The root resource is typically, but not necessarily, a {@link DataStore} instance.
+ * If another root resource existed before this method call, it is discarded without being closed.
+ * Closing the previous resource is caller's responsibility.
*
- * <p>This method updates the {@link #setRoot root} and {@link #setShowRoot showRoot}
- * properties of {@link TreeView}.</p>
+ * <h4>Modified tree view properties</h4>
+ * This method updates the {@link #setRoot root} and {@link #setShowRoot showRoot}
+ * properties of {@link TreeView} in an implementation-dependent way.
*
* @param resource the root resource to show, or {@code null} if none.
+ *
+ * @see #addResource(Resource)
+ * @see #removeAndClose(Resource)
*/
public void setResource(final Resource resource) {
setRoot(resource == null ? null : new Item(resource));
@@ -156,12 +158,16 @@ public class ResourceTree extends TreeView<Resource> {
* has the same effect than invoking {@link #setResource(Resource)}. Otherwise this
* method adds the new resource below previously added resources if not already present.
*
- * <p>This method updates the {@link #setRoot root} and {@link #setShowRoot showRoot}
- * properties of {@link TreeView}.</p>
+ * <h4>Modified tree view properties</h4>
+ * This method updates the {@link #setRoot root} and {@link #setShowRoot showRoot}
+ * properties of {@link TreeView} in an implementation-dependent way.
*
* @param resource the root resource to add, or {@code null} if none.
* @return {@code true} if the given resource has been added, or {@code false}
* if it was already presents or if the given resource is {@code null}.
+ *
+ * @see #setResource(Resource)
+ * @see #removeAndClose(Resource)
*/
public boolean addResource(final Resource resource) {
assert Platform.isFxApplicationThread();
@@ -199,10 +205,16 @@ public class ResourceTree extends TreeView<Resource> {
* If the resource has already been loaded, then this method will use the
* existing instance instead of loading the data again.
*
+ * <h4>Notifications</h4>
+ * If {@link #onResourceLoaded} has a non-null value, the {@link EventHandler} will be
+ * notified in JavaFX thread after the background thread finished to open the resource.
+ * If an exception occurs while opening the resource, then {@link EventHandler} is not
+ * notified and the error is reported in a dialog box instead.
+ *
* @param source the source of the resource to load. This is usually
* a {@link java.io.File} or {@link java.nio.file.Path}.
*
- * @see #onResourceLoaded
+ * @see ResourceExplorer#loadResources(Collection)
*/
public void loadResource(final Object source) {
if (source != null) {
@@ -227,6 +239,10 @@ public class ResourceTree extends TreeView<Resource> {
/**
* Notifies {@link #onResourceLoaded} handler that a resource at the given path has been loaded.
+ * This method is invoked from JavaFX thread.
+ *
+ * <p>Those notifications are not used by {@code ResourceTree} itself.
+ * They are useful to other packages, for example for managing a list of opened files.</p>
*/
private void notifyLoaded(final Object source) {
final EventHandler<LoadEvent> handler = onResourceLoaded.getValue();
@@ -294,13 +310,18 @@ public class ResourceTree extends TreeView<Resource> {
}
/**
- * Removes the given resource from the tree and closes it if it is a {@link DataStore}.
+ * Removes the given resource from this tree and closes the resource if it is a {@link DataStore} instance.
* It is caller's responsibility to ensure that the given resource is not used anymore.
- * A resource can be removed only if it is a root. If the given resource is not in this
- * tree view or is not a root resource, then this method does nothing.
*
- * @param resource the resource to remove, or {@code null}.
+ * <p>Only the "root" resources (such as the resources given to {@link #setResource(Resource)} or
+ * {@link #addResource(Resource)} methods) can be removed.
+ * Children of {@link Aggregate} resource and not scanned.
+ * If the given resource can not be removed, then this method does nothing.</p>
+ *
+ * @param resource the resource to remove. Null values are ignored.
*
+ * @see #setResource(Resource)
+ * @see #addResource(Resource)
* @see ResourceExplorer#removeAndClose(Resource)
*/
public void removeAndClose(final Resource resource) {
@@ -360,24 +381,21 @@ public class ResourceTree extends TreeView<Resource> {
}
/**
- * Returns resources for current locale.
- */
- final Resources localized() {
- return Resources.forLocale(locale);
- }
-
- /**
* Returns a label for a resource. Current implementation returns the
* {@linkplain DataStore#getDisplayName() data store display name} if available,
* or the title found in {@linkplain Resource#getMetadata() metadata} otherwise.
+ * If no label can be found, then this method returns the localized "Unnamed" string.
+ *
+ * <p>This operation may be costly. For example the call to {@link Resource#getMetadata()}
+ * may cause the resource to open a connection to the EPSG database.
+ * Consequently his method should be invoked in a background thread.</p>
*
* @param resource the resource for which to get a label, or {@code null}.
- * @param showError whether to show the error message if an error happen.
+ * @param locale the locale to use for localizing international strings.
* @return the resource display name or the citation title, never null.
*/
- final String getTitle(final Resource resource, final boolean showError) {
- Throwable failure = null;
- if (resource != null) try {
+ static String findLabel(final Resource resource, final Locale locale) throws DataStoreException {
+ if (resource != null) {
final Long logID = LogHandler.loadingStart(resource);
try {
/*
@@ -401,7 +419,7 @@ public class ResourceTree extends TreeView<Resource> {
for (final Identification identification : identifications) {
final Citation citation = identification.getCitation();
if (citation != null) {
- final String t = string(citation.getTitle());
+ final String t = string(citation.getTitle(), locale);
if (t != null) return t;
}
}
@@ -409,12 +427,12 @@ public class ResourceTree extends TreeView<Resource> {
}
/*
* If we find no title in the metadata, use the resource identifier.
- * We search of explicitly declared identifier first before to fallback
+ * We search for explicitly declared identifier first before to fallback
* on metadata, because the later is more subject to interpretation.
*/
final Optional<GenericName> id = resource.getIdentifier();
if (id.isPresent()) {
- final String t = string(id.get().toInternationalString());
+ final String t = string(id.get().toInternationalString(), locale);
if (t != null) return t;
}
if (identifications != null) {
@@ -426,26 +444,21 @@ public class ResourceTree extends TreeView<Resource> {
} finally {
LogHandler.loadingStop(logID);
}
- } catch (DataStoreException | RuntimeException e) {
- if (showError) {
- failure = e;
- }
- }
- /*
- * If we failed to get the name, use "unnamed" with the exception message.
- * It may still be possible to select this resource, view it or expand the children nodes.
- */
- String text = Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed);
- if (failure != null) {
- text = text + " — " + string(failure);
}
- return text;
+ return Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed);
+ }
+
+ /**
+ * Returns resources for current locale.
+ */
+ final Resources localized() {
+ return Resources.forLocale(locale);
}
/**
* Returns the given international string as a non-empty localized string, or {@code null} if none.
*/
- private String string(final InternationalString i18n) {
+ private static String string(final InternationalString i18n, final Locale locale) {
return (i18n != null) ? Strings.trimOrNull(i18n.toString(locale)) : null;
}
@@ -453,7 +466,7 @@ public class ResourceTree extends TreeView<Resource> {
* Returns a localized (if possible) string representation of the given exception.
* This method returns the message if one exist, or the exception class name otherwise.
*/
- private String string(final Throwable failure) {
+ private static String string(final Throwable failure, final Locale locale) {
String text = Strings.trimOrNull(Exceptions.getLocalizedMessage(failure, locale));
if (text == null) {
text = Classes.getShortClassName(failure);
@@ -468,10 +481,14 @@ public class ResourceTree extends TreeView<Resource> {
Logging.unexpectedException(Logging.getLogger(Modules.APPLICATION), ResourceTree.class, method, e);
}
+
+
+
/**
- * The visual appearance of an {@link Item} in a tree. This call gets the cell text from a resource
- * by a call to {@link ResourceTree#getTitle(Resource, boolean)}. Cells are initially empty;
+ * The visual appearance of an {@link Item} in a tree. Cells are initially empty;
* their content will be specified by {@link TreeView} after construction.
+ * This class gets the cell text from a resource by a call to
+ * {@link ResourceTree#findLabel(Resource, Locale)} in a background thread.
* The same call may be recycled many times for different {@link Item} data.
*
* @see Item
@@ -485,59 +502,70 @@ public class ResourceTree extends TreeView<Resource> {
/**
* Invoked when a new resource needs to be shown in the tree view.
- * This method sets the text to a title that describe the resource.
+ * This method sets the text to a label that describe the resource.
*
* @param resource the resource to show.
* @param empty whether this cell is used to fill out space.
*/
@Override
protected void updateItem(final Resource resource, boolean empty) {
- /*
- * This method is sometime invoked even if the resource is the same. It may be for example
- * because the selected state changed. In such case, we do not need to construct again the
- * title, contextual menu, etc. Only the color may change. More generally we don't need to
- * fetch data from enclosing ResourceTree if the resource is the same, so we mark this case
- * by setting `tree` to null.
- */
- final ResourceTree tree = (getItem() != resource) ? (ResourceTree) getTreeView() : null;
super.updateItem(resource, empty); // Mandatory according JavaFX documentation.
- Color color = Styles.NORMAL_TEXT;
- String text = null;
- Button more = null;
- if (!empty) {
- if (resource == PseudoResource.LOADING) {
+ Color color = Styles.NORMAL_TEXT;
+ String text = null;
+ Button more = null;
+ ContextMenu menu = null;
+ final TreeItem<Resource> t;
+ if (!empty && (t = getTreeItem()) instanceof Item) {
+ final ResourceTree tree = (ResourceTree) getTreeView();
+ final Item item = (Item) t;
+ final Throwable error;
+ text = item.label;
+ if (item.isLoading) {
+ /*
+ * If the resource is in process of being loaded in a background thread, show "Loading…"
+ * with a different color. Item with null resource will be replaced by a collection of new
+ * items by a call to `CellItem.getChildren().setAll(…)` after loading process finished.
+ * Item with non-null resource only need to have their name updated.
+ */
color = Styles.LOADING_TEXT;
- if (tree != null) {
- text = tree.localized().getString(Resources.Keys.Loading);
+ if (text == null) {
+ text = item.label = tree.localized().getString(Resources.Keys.Loading);
+ if (resource != null) {
+ item.fetchLabel(resource, tree.locale); // Start a background thread.
+ }
}
- } else if (resource instanceof Unloadable) {
+ } else if ((error = item.error) != null) {
+ /*
+ * If an error occurred, show the exception message with a button for more details.
+ * The list of resource children may or may not be available, depending if the error
+ * occurred while fetching the children list or only their labels.
+ */
color = Styles.ERROR_TEXT;
- if (tree != null) {
- final Throwable failure = ((Unloadable) resource).failure;
- text = tree.string(failure);
- more = new Button(Styles.ERROR_DETAILS_ICON);
- more.setOnAction((e) -> {
- final Resources localized = tree.localized();
- ExceptionReporter.show(tree,
- localized.getString(Resources.Keys.ErrorDetails),
- localized.getString(Resources.Keys.CanNotReadResource), failure);
- });
+ if (text == null) {
+ if (resource != null) {
+ // We have the resource, we only failed to fetch its name.
+ text = Vocabulary.getResources(tree.locale).getString(Vocabulary.Keys.Unnamed);
+ } else {
+ // More serious error (no resource), show exception message.
+ text = string(error, tree.locale);
+ }
+ item.label = text;
}
- } else {
- if (tree != null) {
- text = tree.getTitle(resource, true);
+ more = (Button) getGraphic();
+ if (more == null) {
+ more = new Button(Styles.ERROR_DETAILS_ICON);
}
+ more.setOnAction((e) -> {
+ final Resources localized = tree.localized();
+ ExceptionReporter.show(tree,
+ localized.getString(Resources.Keys.ErrorDetails),
+ localized.getString(Resources.Keys.CanNotReadResource), error);
+ });
}
- }
- setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
- /*
- * If the resource is at the root, add a menu for removing it.
- * If we find that the cell already has a menu, we do not need to build it again.
- */
- if (tree != null) {
- setText(text);
- setGraphic(more);
- ContextMenu menu = null;
+ /*
+ * If the resource is one of the "root" resources, add a menu for removing it.
+ * If we find that the cell already has a menu, we do not need to build it again.
+ */
if (tree.findOrRemove(resource, false)) {
menu = getContextMenu();
if (menu == null) {
@@ -563,8 +591,11 @@ public class ResourceTree extends TreeView<Resource> {
}
menu.getItems().get(COPY_PATH).setDisable(!IOUtilities.isKindOfPath(path));
}
- setContextMenu(menu);
}
+ setText(text);
+ setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
+ setGraphic(more);
+ setContextMenu(menu);
}
/**
@@ -574,15 +605,41 @@ public class ResourceTree extends TreeView<Resource> {
private static final int COPY_PATH = 0, CLOSE = 1;
}
+
+
+
/**
- * A simple node encapsulating a {@link Resource} in a view.
- * The list of children is fetched when first needed.
+ * An item of the {@link Resource} tree completed with additional information.
+ * The list of children is fetched in a background thread when first needed.
* This node contains only the data; for visual appearance, see {@link Cell}.
*
* @see Cell
*/
private static final class Item extends TreeItem<Resource> {
/**
+ * The text of this node, computed and cached when first needed.
+ * Computation is done by invoking {@link #findLabel(Resource, Locale)} in a background thread.
+ *
+ * @see #fetchLabel(Resource, Locale)
+ */
+ String label;
+
+ /**
+ * Whether this node is in process of loading data. There is two kinds of loading:
+ * <ul>
+ * <li>The {@link Resource} itself, in which case {@link #getValue()} is null.</li>
+ * <li>The resource {@link #title}, in which case {@link #getValue()} has a valid value.</li>
+ * </ul>
+ */
+ boolean isLoading;
+
+ /**
+ * If an error occurred while loading the resource, the cause. The {@link #getValue()} property may
+ * be null or non-null, depending if the error occurred while loading the resource or only its title.
+ */
+ Throwable error;
+
+ /**
* Whether the resource is a leaf. A resource is a leaf if it is not an
* instance of {@link Aggregate}, in which case it can not have children.
* This information is cached because requested often.
@@ -599,17 +656,62 @@ public class ResourceTree extends TreeView<Resource> {
private boolean isChildrenKnown;
/**
+ * Creates a temporary item with null value for a resource in process of being loaded.
+ * This item will be replaced (not updated) by a fresh {@code Item} instance when the
+ * resource will become available.
+ */
+ Item() {
+ isLeaf = true;
+ isLoading = true;
+ }
+
+ /**
+ * Creates an item for a resource that we failed to load.
+ */
+ Item(final Throwable exception) {
+ isLeaf = true;
+ error = exception;
+ }
+
+ /**
* Creates a new node for the given resource.
*
* @param resource the resource to show in the tree.
*/
Item(final Resource resource) {
super(resource);
+ isLoading = true; // Means that the label still need to be fetched.
isLeaf = !(resource instanceof Aggregate);
LogHandler.installListener(resource);
}
/**
+ * Update {@link #label} with the resource label fetched in background thread.
+ * Caller should invoke this method only if {@link #isLoading} is {@code true}
+ * {@link #label} is null. Note that {@link #error} should be null in such case.
+ */
+ final void fetchLabel(final Resource resource, final Locale locale) {
+ BackgroundThreads.execute(new Task<String>() {
+ @Override protected String call() throws DataStoreException {
+ return findLabel(resource, locale);
+ }
+
+ @Override protected void succeeded() {
+ isLoading = false;
+ label = getValue();
+ GUIUtilities.forceCellUpdate(Item.this);
+ }
+
+ @Override protected void failed() {
+ isLoading = false;
+ label = null;
+ error = getException();
+ GUIUtilities.forceCellUpdate(Item.this);
+ }
+ });
+ }
+
+ /**
* Returns whether the resource can not have children.
*/
@Override
@@ -619,7 +721,7 @@ public class ResourceTree extends TreeView<Resource> {
/**
* Returns the items for all sub-resources contained in this resource.
- * The list is empty is the resource is not an aggregate.
+ * The list is empty if the resource is not an aggregate.
*/
@Override
public ObservableList<TreeItem<Resource>> getChildren() {
@@ -629,7 +731,7 @@ public class ResourceTree extends TreeView<Resource> {
final Resource resource = getValue();
if (resource instanceof Aggregate) {
BackgroundThreads.execute(new GetChildren((Aggregate) resource));
- children.add(new Item(PseudoResource.LOADING)); // Temporary node with "loading" text.
+ children.add(new Item());
}
}
return children;
@@ -671,53 +773,32 @@ public class ResourceTree extends TreeView<Resource> {
/**
* Invoked in JavaFX thread if children have been loaded successfully.
+ * The previous node, which was showing "Loading…", is replaced by all
+ * nodes loaded in the background thread.
*/
@Override
protected void succeeded() {
- setResources(getValue());
+ Item.super.getChildren().setAll(getValue());
}
/**
* Invoked in JavaFX thread if children can not be loaded.
- * This method set a placeholder items with error message.
*/
@Override
protected void failed() {
- setResources(Collections.singletonList(new Item(new Unloadable(getException()))));
+ Item.super.getChildren().setAll(new Item(getException()));
}
}
-
- /**
- * Sets the resources after the background task completed.
- * This method must be invoked in the JavaFX thread.
- */
- private void setResources(final List<TreeItem<Resource>> result) {
- super.getChildren().setAll(result);
- }
}
- /**
- * Placeholder for a resource that we failed to load.
- */
- private static final class Unloadable extends PseudoResource {
- /**
- * The reason why we can not load the resource.
- */
- final Throwable failure;
- /**
- * Creates a new place-holder for a resource that we failed to load for the given reason.
- */
- Unloadable(final Throwable failure) {
- this.failure = failure;
- }
- }
+
/**
* The root resource when there is more than one resources to display.
* This root node should be hidden in the {@link ResourceTree}.
*/
- private static final class Root extends PseudoResource implements Aggregate {
+ private static final class Root implements Aggregate {
/**
* The children to expose as an unmodifiable list of components.
*/
@@ -758,7 +839,7 @@ public class ResourceTree extends TreeView<Resource> {
* @return whether the given resource has been added.
*/
boolean add(final Resource resource) {
- for (int i=components.size(); --i >= 0;) {
+ for (int i = components.size(); --i >= 0;) {
if (components.get(i).getValue() == resource) {
return false;
}
@@ -793,24 +874,6 @@ public class ResourceTree extends TreeView<Resource> {
}
};
}
- }
-
- /**
- * A pseudo-resource with no identifier and no metadata.
- * This is used as a placeholder for a node while loading
- * is in progress, or for reporting a failure to load a node.
- */
- private static class PseudoResource implements Resource {
- /**
- * Place holder for a resource in process of being loaded.
- */
- static final PseudoResource LOADING = new PseudoResource();
-
- /**
- * Creates a new pseudo-resource.
- */
- PseudoResource() {
- }
/**
* Returns empty optional since this resource has no identifier.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
index 3d8bfe9..f8c4bc4 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -26,6 +26,7 @@ import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
+import javafx.scene.control.TreeItem;
import javafx.scene.shape.Rectangle;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
@@ -37,13 +38,14 @@ import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.measure.Quantities;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Static;
+import org.apache.sis.util.Workaround;
/**
* Miscellaneous utility methods.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -97,6 +99,21 @@ public final class GUIUtilities extends Static {
}
/**
+ * Forces a {@link TreeItem} to update the {@code TreeView} when its value has been externally modified.
+ * This is a workaround for situations where the item's value is unchanged, but some state of the value
+ * has been modified.
+ *
+ * @param <T> type of values in the tree item.
+ * @param item the item for which to force an update.
+ */
+ @Workaround(library = "JavaFX", version = "17")
+ public static <T> void forceCellUpdate(final TreeItem<T> item) {
+ final T value = item.getValue();
+ item.setValue(null);
+ item.setValue(value);
+ }
+
+ /**
* Copies all elements from the given source list to the specified target list,
* but with the application of insertion and removal operations only.
* This method is useful when the two lists should be similar.