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 d38375dcb3 Consolidation of `ResourceTree` making a better usage of 
JavaFX events. This work is needed for reusing some properties in a future 
widget for configuring map items.
d38375dcb3 is described below

commit d38375dcb340558d1f094d32d397b57dba09ca56
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Mar 26 21:07:59 2026 +0100

    Consolidation of `ResourceTree` making a better usage of JavaFX events.
    This work is needed for reusing some properties in a future widget for 
configuring map items.
---
 .../org/apache/sis/gui/dataset/ResourceCell.java   | 138 +++++++++-------
 .../apache/sis/gui/dataset/ResourceExplorer.java   |   9 +-
 .../org/apache/sis/gui/dataset/ResourceItem.java   | 178 +++++++++++++--------
 .../org/apache/sis/gui/dataset/ResourceTree.java   |  62 +++++--
 .../org/apache/sis/gui/dataset/RootResource.java   |  24 +--
 .../apache/sis/gui/internal/DataStoreOpener.java   |   1 +
 .../org/apache/sis/gui/internal/GUIUtilities.java  |  16 --
 7 files changed, 256 insertions(+), 172 deletions(-)

diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
index 29fed2fa50..5192d2e5ff 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
@@ -16,21 +16,19 @@
  */
 package org.apache.sis.gui.dataset;
 
-import java.util.Locale;
 import javafx.collections.ObservableList;
 import javafx.scene.control.Button;
 import javafx.scene.control.ContextMenu;
 import javafx.scene.control.MenuItem;
 import javafx.scene.control.CheckMenuItem;
 import javafx.scene.control.TreeCell;
-import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
 import javafx.scene.paint.Color;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.gui.internal.ExceptionReporter;
-import org.apache.sis.gui.internal.DataStoreOpener;
 import org.apache.sis.gui.internal.Resources;
 import org.apache.sis.gui.internal.Styles;
 import org.apache.sis.util.Classes;
@@ -42,9 +40,8 @@ import org.apache.sis.util.resources.Vocabulary;
 /**
  * The visual appearance of an {@link ResourceItem} 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 DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
- * The same call may be recycled many times for different {@link ResourceItem} 
data.
+ * The text is initially "Loading…" and the actual text is obtained from the 
resource in a background thread.
+ * The same {@code ResourceCell} instance may be recycled many times for 
different {@link ResourceItem} data.
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
@@ -52,21 +49,26 @@ import org.apache.sis.util.resources.Vocabulary;
  */
 final class ResourceCell extends TreeCell<Resource> {
     /**
-     * The type of view (original resource, aggregated resources, etc.) shown 
in this node.
+     * Position of menu items in the contextual menu built by {@link 
#updateItem(Resource, boolean)}.
+     * Above method assumes that {@link #CLOSE} is the last menu item.
      */
-    private TreeViewType viewType;
+    private static final int COPY_PATH = 0, OPEN_FOLDER = 1, AGGREGATED = 2, 
CLOSE = 3;
 
     /**
      * Creates a new cell with initially no data.
+     *
+     * @param  tree  the tree which will contain this new cell
+     *         (ignored, defined for matching method signature of call 
factory).
      */
-    ResourceCell() {
+    @SuppressWarnings("unused")
+    ResourceCell(final TreeView<Resource> tree) {
     }
 
     /**
      * Invoked when a new resource needs to be shown in the tree view.
-     * This method sets the text to a label that describe the resource
-     * (possibly starting a background thread for fetching that label)
-     * and set a contextual menu.
+     * This method sets the text to a label that describes the resource,
+     * possibly starting a background thread for fetching that label.
+     * This method also sets a contextual menu.
      *
      * @param resource  the resource to show.
      * @param empty     whether this cell is used to fill out space.
@@ -75,16 +77,12 @@ final class ResourceCell extends TreeCell<Resource> {
     protected void updateItem(final Resource resource, boolean empty) {
         super.updateItem(resource, empty);          // Mandatory according 
JavaFX documentation.
         Color       color = Styles.NORMAL_TEXT;
-        String      text  = null;
         Button      more  = null;
         ContextMenu menu  = null;
-        final TreeItem<Resource> t;
-        if (!empty && (t = getTreeItem()) instanceof ResourceItem) {
-            final ResourceTree tree = (ResourceTree) getTreeView();
-            final ResourceItem item = (ResourceItem) t;
-            final Throwable error;
-            text = item.label;
-            if (item.isLoading) {
+        if (!empty && getTreeItem() instanceof ResourceItem item) {
+            final var tree = (ResourceTree) getTreeView();
+            textProperty().bind(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
@@ -92,40 +90,23 @@ final class ResourceCell extends TreeCell<Resource> {
                  * Item with non-null resource only need to have their name 
updated.
                  */
                 color = Styles.LOADING_TEXT;
-                if (text == null) {
-                    text = item.label = 
tree.localized().getString(Resources.Keys.Loading);
+                if (item.label.getValue() == null) {
+                    
item.label.setValue(tree.localized().getString(Resources.Keys.Loading));
                     if (resource != null) {
-                        tree.fetchLabel(item.new Completer(resource));      // 
Start a background thread.
+                        tree.fetchLabel(item.new Completer(resource, this));  
// Start a background thread.
                     }
                 }
-            } else if ((error = item.error) != null) {
+            } else {
                 /*
                  * 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 (text == null) {
-                    if (resource != null) {
-                        // We have the resource, we only failed to fetch its 
name.
-                        text = 
Vocabulary.forLocale(tree.locale).getString(Vocabulary.Keys.Unnamed);
-                    } else {
-                        // More serious error (no resource), show exception 
message.
-                        text = 
Strings.trimOrNull(Exceptions.getLocalizedMessage(error, tree.locale));
-                        if (text == null) text = 
Classes.getShortClassName(error);
-                    }
-                    item.label = text;
-                }
-                more = (Button) getGraphic();
-                if (more == null) {
-                    more = new Button(Styles.ERROR_DETAILS_ICON);
+                final Throwable error = item.error();
+                if (error != null) {
+                    color = Styles.ERROR_TEXT;
+                    more = createErrorDetails(tree, item, error);
                 }
-                more.setOnAction((e) -> {
-                    final Resources localized = tree.localized();
-                    ExceptionReporter.show(tree,
-                            localized.getString(Resources.Keys.ErrorDetails),
-                            
localized.getString(Resources.Keys.CanNotReadResource), error);
-                });
             }
             /*
              * Following block is for the contextual menu. In current version,
@@ -170,18 +151,67 @@ final class ResourceCell extends TreeCell<Resource> {
                 aggregated.setDisable(!aggregatable);
                 aggregated.setSelected(aggregatable && 
item.isView(TreeViewType.AGGREGATION));
             }
+        } else {
+            textProperty().unbind();
+            setText(null);
         }
-        setText(text);
         setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
         setGraphic(more);
         setContextMenu(menu);
     }
 
     /**
-     * Position of menu items in the contextual menu built by {@link 
#updateItem(Resource, boolean)}.
-     * Above method assumes that {@link #CLOSE} is the last menu item.
+     * Invoked after a loading has been completed, either successfully or with 
an error.
+     * If this cell view is no longer showing the given item (for example if 
the content
+     * changed concurrently), then this method does nothing.
+     *
+     * @param  item   the item in which the error occurred.
+     * @param  error  the error that occurred, or {@code null} if the 
operation was successful.
      */
-    private static final int COPY_PATH = 0, OPEN_FOLDER = 1, AGGREGATED = 2, 
CLOSE = 3;
+    final void completed(final ResourceItem item, final Throwable error) {
+        if (item == getTreeItem()) {
+            Color color = Styles.NORMAL_TEXT;
+            if (error != null) {
+                color = Styles.ERROR_TEXT;
+                setGraphic(createErrorDetails((ResourceTree) getTreeView(), 
item, error));
+            }
+            setTextFill(isSelected() ? Styles.SELECTED_TEXT : color);
+        }
+    }
+
+    /**
+     * Creates a button for providing details about an exception that occurred 
while loading the data.
+     * This method also updates the label of the given item with a text that 
summarizes the error.
+     *
+     * @param  tree   value of {@link #getTreeView()}.
+     * @param  item   the item in which the error occurred.
+     * @param  error  the error that occurred.
+     */
+    private Button createErrorDetails(final ResourceTree tree, final 
ResourceItem item, final Throwable error) {
+        if (item.label.getValue() == null) {
+            String text;
+            if (item.getValue() != null) {
+                // We have the resource, we only failed to fetch its name.
+                text = 
Vocabulary.forLocale(tree.locale).getString(Vocabulary.Keys.Unnamed);
+            } else {
+                // More serious error (no resource), show exception message.
+                text = 
Strings.trimOrNull(Exceptions.getLocalizedMessage(error, tree.locale));
+                if (text == null) text = Classes.getShortClassName(error);
+            }
+            item.label.setValue(text);
+        }
+        Button 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);
+        });
+        return more;
+    }
 
     /**
      * Sets the view of the resource to show in this node.
@@ -189,16 +219,6 @@ final class ResourceCell extends TreeCell<Resource> {
      * we can create an aggregated view of all components.
      */
     private void setView(final TreeViewType type) {
-        viewType = type;
         ((ResourceItem) getTreeItem()).setView(this, type, ((ResourceTree) 
getTreeView()).locale);
     }
-
-    /**
-     * Returns whether the specified view is the currently active view.
-     * This is used for detecting if users changed their selection again
-     * while computation was in progress in the background thread.
-     */
-    final boolean isActiveView(final TreeViewType type) {
-        return viewType == type;
-    }
 }
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceExplorer.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceExplorer.java
index a2c8c5f3fb..711b1c3dd9 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -60,9 +60,7 @@ import org.apache.sis.gui.internal.LogHandler;
 
 
 /**
- * A panel showing a {@linkplain ResourceTree tree of resources} together with 
their metadata and data views.
- * This panel also contains a "new window" button for creating new windows 
showing the same data but potentially
- * a different locations and times. {@code ResourceExplorer} contains a list 
of windows created by this widget.
+ * A panel showing a tree of resources together with their metadata and data 
views.
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
@@ -150,8 +148,8 @@ public class ResourceExplorer extends Widget {
     private final Accordion controls;
 
     /**
-     * The control that put everything together.
-     * The type of control may change in any future SIS version.
+     * The controls for choosing a resource or configuring its view (left) 
together with
+     * the visualization of the selected resource (right).
      *
      * @see #getView()
      */
@@ -229,7 +227,6 @@ public class ResourceExplorer extends Widget {
         content = new SplitPane(controls, tabs);
         content.setDividerPosition(0, 1./3);
         SplitPane.setResizableWithParent(controls, Boolean.FALSE);
-        SplitPane.setResizableWithParent(tabs,     Boolean.TRUE);
         /*
          * Register listeners last, for making sure we do not have undesired 
events.
          * Those listeners trig loading of various objects (data, standard 
metadata,
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceItem.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceItem.java
index efd460f5a2..47af0b4c88 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceItem.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceItem.java
@@ -22,6 +22,8 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.EnumMap;
 import javafx.application.Platform;
+import javafx.beans.property.StringProperty;
+import javafx.beans.property.SimpleStringProperty;
 import javafx.concurrent.Task;
 import javafx.collections.ObservableList;
 import javafx.scene.control.TreeItem;
@@ -32,59 +34,69 @@ import org.apache.sis.storage.aggregate.MergeStrategy;
 import org.apache.sis.storage.folder.UnstructuredAggregate;
 import org.apache.sis.gui.internal.DataStoreOpener;
 import org.apache.sis.gui.internal.BackgroundThreads;
-import org.apache.sis.gui.internal.GUIUtilities;
 import org.apache.sis.gui.internal.LogHandler;
 
 
 /**
- * An item of the {@link Resource} tree completed with additional information.
+ * An item of the {@link ResourceTree} completed with additional information.
  * The {@linkplain #getChildren() list of children} is fetched in a background 
thread when first needed.
- * This node contains only the data; for visual appearance, see {@link 
ResourceCell}.
+ * This node contains only the data. For visual appearance, see {@link 
ResourceCell}.
+ *
+ * <p>The initial {@link Resource} value of this item is usually {@code null} 
and should be set only once.
+ * Resource shall be set by a call to {@link #setValue(Resource, String)} 
instead of {@code setValue(T)}.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
- * @see Cell
+ * @see ResourceCell
  */
 final class ResourceItem extends TreeItem<Resource> {
     /**
-     * The path to the resource, or {@code null} if none or unknown. This is 
used for notifications only;
-     * this information does not play an important role for {@link 
ResourceTree} itself.
+     * The path to the resource, or {@code null} if none or unknown.
+     * This is used by {@link ResourceTree} merely for notifications.
      */
     Path path;
 
     /**
-     * The text of this node, computed and cached when first needed. 
Computation is done by invoking
+     * The text of this node, computed when first needed. Computation is done 
by invoking
      * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
+     * May contain a temporary text such as "Loading…".
      *
      * @see ResourceTree#fetchLabel(ResourceItem.Completer)
      */
-    String label;
+    final StringProperty label = new SimpleStringProperty();
 
     /**
      * Whether this node is in process of loading data. There are 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>
+     *   <li>The {@link Resource} itself, in which case {@link #getValue()} is 
null until the loading is completed.</li>
+     *   <li>The resource {@link #label}, in which case {@link #getValue()} 
has a valid value.</li>
      * </ul>
+     *
+     * @see #isLoading()
      */
-    boolean isLoading;
+    private 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.
+     * be null or non-null, depending if the error occurred while loading the 
resource or only its label.
+     *
+     * @see #error()
      */
-    Throwable error;
+    private 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 cannot have children.
      * This information is cached because requested often.
+     *
+     * @see #isLeaf()
      */
-    private final boolean isLeaf;
+    private boolean isLeaf;
 
     /**
      * Whether the list of children has been determined. We use this flag in 
order
      * to fetch children only when first requested, since this process is 
costly.
+     * This flag is ignored if {@link #isLeaf} is {@code true}.
      *
      * @todo Register {@link org.apache.sis.storage.event.StoreListener} and 
reset
      *       this flag to {@code false} if the resource content or structure 
changed.
@@ -92,55 +104,88 @@ final class ResourceItem extends TreeItem<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 
ResourceItem} instance
-     * when the resource will become available.
+     * Creates an item with null value for a resource in process of being 
loaded.
+     * The {@linkplain #label} should be "Loading…", but this is not set by 
this constructor.
+     * Instead, it will be set by {@link ResourceCell} the first time that 
this item will be shown.
      */
-    ResourceItem() {
+    private ResourceItem() {
         isLeaf    = true;
         isLoading = true;
     }
 
     /**
      * Creates an item for a resource that we failed to load.
+     * This constructor is used when all previous items are discarded.
+     * It happens when we failed to load the components of an aggregate.
+     *
+     * <p>The {@linkplain #label} should be the error message, but this is not 
set by this constructor.
+     * Instead, it will be set by {@link ResourceCell} the first time that 
this item will be shown.</p>
      */
-    private ResourceItem(final Throwable exception) {
+    private ResourceItem(final Throwable failure) {
         isLeaf = true;
-        error  = exception;
+        error  = failure;
     }
 
     /**
      * Creates a new node for the given resource.
+     * The {@linkplain #label} should be the resource name, but this is not 
set by this constructor.
+     * Instead, it will be set by {@link ResourceCell} by fetching the name in 
a background thread.
      *
      * @param resource  the resource to show in the tree.
      */
     ResourceItem(final Resource resource) {
         super(resource);
         isLoading = true;       // Means that the label still need to be 
fetched.
-        isLeaf    = !(resource instanceof Aggregate);
+        configure(resource);
+    }
+
+    /**
+     * Updates the internal fields of this item for a new resource.
+     * Also redirects logging messages emitted by the resource.
+     */
+    private void configure(final Resource resource) {
+        isLeaf = !(resource instanceof Aggregate);
         LogHandler.installListener(resource);
     }
 
+    /**
+     * Sets the resource after the loading in a background thread completed 
successfully.
+     *
+     * @param resource  the resource to show in the tree.
+     * @param text      the text to show as the resource's label.
+     */
+    public void setValue(final Resource resource, final String text) {
+        isLoading = false;
+        label.setValue(text);
+        configure(resource);
+        setValue(resource);
+    }
+
     /**
      * Update {@link #label} with the resource label fetched in background 
thread.
-     * Caller should use this task only if {@link #isLoading} is {@code true}.
+     * Caller should use this task only if {@link #isLoading()} is {@code 
true}.
      */
     final class Completer implements Runnable {
         /** The resource for which to fetch a label. */
         private final Resource resource;
 
+        /** The cell where the resource will be shown in the tree. */
+        private final ResourceCell cell;
+
         /** Result of fetching the label of a resource. */
         private String result;
 
         /** Error that occurred while fetching the label. */
         private Throwable failure;
 
-        /** Creates a new container for the label of a resource. */
-        Completer(final Resource resource) {
+        /** Creates a new task for fetching the label of a resource. */
+        Completer(final Resource resource, final ResourceCell cell) {
             this.resource = resource;
+            this.cell = cell;
         }
 
         /** Invoked in a background thread for fetching the label. */
+        @SuppressWarnings("UseSpecificCatch")
         final void fetch(final Locale locale) {
             try {
                 result = DataStoreOpener.findLabel(resource, locale, false);
@@ -150,15 +195,29 @@ final class ResourceItem extends TreeItem<Resource> {
             Platform.runLater(this);
         }
 
-        /** Invoked in JavaFX thread after the label has been fetched. */
+        /** Invoked in JavaFX thread after the label has been fetched or 
failed to be fetched. */
         @Override public void run() {
             isLoading = false;
-            label     = result;
-            error     = failure;
-            GUIUtilities.forceCellUpdate(ResourceItem.this);
+            label.setValue(result);
+            if (failure != null) error = failure;
+            cell.completed(ResourceItem.this, failure);
         }
     }
 
+    /**
+     * If an error occurred while loading the resource, the exception which 
was thrown.
+     */
+    final Throwable error() {
+        return error;
+    }
+
+    /**
+     * Returns whether this node is in process of loading data.
+     */
+    final boolean isLoading() {
+        return isLoading;
+    }
+
     /**
      * Returns whether the resource cannot have children.
      */
@@ -207,7 +266,7 @@ final class ResourceItem extends TreeItem<Resource> {
          */
         @Override
         protected List<TreeItem<Resource>> call() throws DataStoreException {
-            final List<TreeItem<Resource>> items = new ArrayList<>();
+            final var items = new ArrayList<TreeItem<Resource>>();
             final Long id = LogHandler.loadingStart(resource);
             try {
                 for (final Resource component : resource.components()) {
@@ -226,7 +285,7 @@ final class ResourceItem extends TreeItem<Resource> {
          */
         @Override
         protected void succeeded() {
-            ResourceItem.super.getChildren().setAll(getValue());
+            getChildren().setAll(getValue());
         }
 
         /**
@@ -237,7 +296,7 @@ final class ResourceItem extends TreeItem<Resource> {
         @Override
         @SuppressWarnings("unchecked")
         protected void failed() {
-            ResourceItem.super.getChildren().setAll(new 
ResourceItem(getException()));
+            getChildren().setAll(new ResourceItem(getException()));
         }
     }
 
@@ -253,7 +312,7 @@ final class ResourceItem extends TreeItem<Resource> {
      * Otherwise {@code null}. This is used for switching view without 
recomputing the resource.
      * All {@link ResourceItem} derived from the same source will share the 
same map of views.
      */
-    private EnumMap<TreeViewType,ResourceItem> views;
+    private EnumMap<TreeViewType, ResourceItem> views;
 
     /**
      * Returns the resource which is the source of this item.
@@ -267,12 +326,12 @@ final class ResourceItem extends TreeItem<Resource> {
      * This method should be used instead of {@code getValue() == resource} 
for locating the item
      * that represents a resource.
      */
-    final boolean contains(final Resource resource) {
-        if (getValue() == resource) {
+    static boolean isWrapperOf(final TreeItem<Resource> item, final Resource 
resource) {
+        if (item.getValue() == resource) {
             return true;
         }
-        if (views != null) {
-            for (final ResourceItem view : views.values()) {
+        if (item instanceof ResourceItem r && r.views != null) {
+            for (final ResourceItem view : r.views.values()) {
                 if (view.getValue() == resource) {
                     return true;
                 }
@@ -341,23 +400,6 @@ final class ResourceItem extends TreeItem<Resource> {
         siblings.add(view);
     }
 
-    /**
-     * Replaces this resource item by a newly created view.
-     * This method must be invoked on the item to replace,
-     * which may be the placeholder for the "loading" label.
-     *
-     * @param  cell  the cell which is requesting a view.
-     * @param  type  type of the newly created view.
-     * @param  view  the newly created view to select as the active view.
-     */
-    private void setNewView(final ResourceCell cell, final TreeViewType type, 
final ResourceItem view) {
-        view.views = views;
-        views.put(type, view);
-        if (cell == null || cell.isActiveView(type)) {
-            selectView(view);
-        }
-    }
-
     /**
      * Enables or disables the aggregated view. This functionality is used 
mostly when the resource is a folder,
      * for example added by a drag-and-drop action. It usually do not apply to 
individual files.
@@ -376,12 +418,22 @@ final class ResourceItem extends TreeItem<Resource> {
             selectView(existing);
             return;
         }
+        /*
+         * Replaces this resource item by a newly created view.
+         * The new item will initially show only "Loading…".
+         * The actual label is fetched in a background thread.
+         */
         final Resource resource = getSource();
-        final ResourceItem loading = new ResourceItem();
-        setNewView(null, type, loading);
-        BackgroundThreads.execute(new Task<ResourceItem>() {
+        final var loading = new ResourceItem();
+        loading.views = views;
+        views.put(type, loading);
+        selectView(loading);
+        BackgroundThreads.execute(new Task<Resource>() {
+            /** Value to assign to the label property. */
+            private String text;
+
             /** Fetch in a background thread the view selected by user. */
-            @Override protected ResourceItem call() throws DataStoreException {
+            @Override protected Resource call() throws DataStoreException {
                 Resource result = resource;
                 switch (type) {
                     case AGGREGATION: {
@@ -394,20 +446,20 @@ final class ResourceItem extends TreeItem<Resource> {
                     // More cases may be added in the future.
                 }
                 LogHandler.redirect(result, resource);
-                final ResourceItem item = new ResourceItem(result);
-                item.label = DataStoreOpener.findLabel(resource, locale, 
false);
-                item.isLoading = false;
-                return item;
+                text = DataStoreOpener.findLabel(resource, locale, false);
+                return result;
             }
 
             /** Invoked in JavaFX thread after the requested view has been 
obtained. */
             @Override protected void succeeded() {
-                loading.setNewView(cell, type, getValue());
+                loading.setValue(getValue(), text);
             }
 
             /** Invoked in JavaFX thread if an exception occurred while 
fetching the view. */
             @Override protected void failed() {
-                loading.setNewView(cell, type, new 
ResourceItem(getException()));
+                loading.isLoading = false;
+                loading.isLeaf = true;
+                cell.completed(loading, getException());
             }
         });
     }
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceTree.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceTree.java
index c35cb2c5c8..8f472966a4 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceTree.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceTree.java
@@ -32,6 +32,7 @@ import javafx.concurrent.Task;
 import javafx.collections.ObservableList;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.ReadOnlyStringProperty;
 import javafx.event.EventHandler;
 import javafx.scene.control.TreeItem;
 import javafx.scene.control.TreeView;
@@ -52,13 +53,13 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER;
 
 
 /**
- * A view of data {@link Resource}s organized as a tree.
+ * A view of data store resources organized in a tree.
  * This view can be used for showing the content of one or many {@link 
DataStore}s.
  * A resource can be added by a call to {@link #addResource(Resource)} or 
loaded from
  * a file by {@link #loadResource(Object)}.
  *
  * <p>{@code ResourceTree} registers the necessarily handlers for making this 
view a target
- * of "drag and drop" events. Users can drop files or URLs for opening data 
files.</p>
+ * of "drag and drop" events. Users can drop files or <abbr>URL</abbr>s for 
opening data files.</p>
  *
  * <h2>Limitations</h2>
  * <ul>
@@ -69,9 +70,11 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER;
  *       if the resource is shared by another {@link ResourceTree} 
instance.</li>
  * </ul>
  *
+ * @todo We should remove automatically the {@code DataStore} instances that 
are closed externally.
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
  * @since   1.1
  */
 public class ResourceTree extends TreeView<Resource> {
@@ -81,7 +84,7 @@ public class ResourceTree extends TreeView<Resource> {
     final Locale locale;
 
     /**
-     * Function to be called after a resource has been loaded from a file or 
URL.
+     * Function to be called after a resource has been loaded from a file or 
<abbr>URL</abbr>.
      * The default value is {@code null}.
      *
      * @see #loadResource(Object)
@@ -90,7 +93,9 @@ public class ResourceTree extends TreeView<Resource> {
     public final ObjectProperty<EventHandler<ResourceEvent>> onResourceLoaded;
 
     /**
-     * Function to be called after a resource has been closed from a file or 
URL.
+     * Function to be called after a resource has been closed and removed from 
this tree view.
+     * This event happens when the {@link #removeAndClose(Resource)} method 
has been invoked,
+     * which happens for example when the user selected the "close" item from 
the contextual menu.
      * The default value is {@code null}.
      *
      * @see #removeAndClose(Resource)
@@ -106,9 +111,9 @@ public class ResourceTree extends TreeView<Resource> {
      * All accesses to this list must be synchronized on {@code pendingItems}.
      *
      * <h4>Design note</h4>
-     * We use a list instead of creating a {@link Task} for each item because 
the latter can create a lot
+     * We use a queue instead of creating a {@link Task} for each item because 
the latter can create a lot
      * of threads, which are likely to be blocked anyway because of {@link 
DataStore} synchronization.
-     * Furthermore, those threads of overkill in the common case where labels 
are very quick to fetch.
+     * Furthermore, those threads are overkill in the common case where labels 
are very quick to fetch.
      *
      * @see #fetchLabel(ResourceItem.Completer)
      */
@@ -116,13 +121,16 @@ public class ResourceTree extends TreeView<Resource> {
 
     /**
      * Creates a new tree of resources with initially no resource to show.
-     * For showing a resource, invoke {@link #setResource(Resource)} after 
construction.
+     * For showing a resource, invoke
+     * {@link #setResource(Resource)},
+     * {@link #addResource(Resource)} or
+     * {@link #loadResource(Object)} after construction.
      */
     @SuppressWarnings("this-escape")    // `this` appears in a cyclic graph.
     public ResourceTree() {
         locale = Locale.getDefault();
         pendingItems = new LinkedList<>();
-        setCellFactory((v) -> new ResourceCell());
+        setCellFactory(ResourceCell::new);
         setOnDragOver(ResourceTree::onDragOver);
         setOnDragDropped(this::onDragDropped);
         onResourceLoaded = new SimpleObjectProperty<>(this, 
"onResourceLoaded");
@@ -130,7 +138,7 @@ public class ResourceTree extends TreeView<Resource> {
     }
 
     /**
-     * Returns the root {@link Resource} of this tree.
+     * Returns the root data store {@code Resource} of this tree.
      * The returned value depends on how the resource was set:
      *
      * <ul>
@@ -149,7 +157,7 @@ public class ResourceTree extends TreeView<Resource> {
     }
 
     /**
-     * Sets the root {@link Resource} of this tree.
+     * Sets the root data store {@code 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.
@@ -205,7 +213,7 @@ public class ResourceTree extends TreeView<Resource> {
          * but it was causing confusing events when the second resource was 
added.
          */
         if (addTo == null) {
-            final TreeItem<Resource> group = new TreeItem<>();
+            final var group = new TreeItem<Resource>();
             setShowRoot(false);
             setRoot(group);                                 // Also detach 
`item` from the TreeView root.
             addTo = new RootResource(group, item);          // Pseudo-resource 
for a group of data stores.
@@ -236,7 +244,7 @@ public class ResourceTree extends TreeView<Resource> {
             if (source instanceof Resource) {
                 addResource((Resource) source);
             } else {
-                final DataStoreOpener opener = new DataStoreOpener(source);
+                final var opener = new DataStoreOpener(source);
                 final DataStore existing = opener.fromCache();
                 if (existing != null) {
                     addResource(existing);
@@ -333,6 +341,26 @@ public class ResourceTree extends TreeView<Resource> {
         event.consume();
     }
 
+    /**
+     * Returns the text which is shown in the <abbr>GUI</abbr> for the given 
resource.
+     * This returned property may have a temporarily {@code null} value or a 
placeholder such as "Loading…",
+     * then be updated to its final value after a background process finished 
to fetch the resource's label.
+     *
+     * <p>This method must be invoked from the JavaFX application thread.</p>
+     *
+     * @param  resource  the resource for which to get the label, or {@code 
null}.
+     * @return the label of the given resource, or {@code null} if the 
resource has not been found.
+     *
+     * @since 1.7
+     */
+    public ReadOnlyStringProperty getLabelOf(final Resource resource) {
+        final TreeItem<Resource> item = findOrRemove(resource, false);
+        if (item instanceof ResourceItem) {
+            return ((ResourceItem) item).label;
+        }
+        return null;
+    }
+
     /**
      * 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.
@@ -387,6 +415,8 @@ public class ResourceTree extends TreeView<Resource> {
      * If {@code remove} is {@code true}, then it is caller's responsibility 
to close
      * the resource.
      *
+     * <p>This method must be invoked from the JavaFX application thread.</p>
+     *
      * @param  resource  the resource to search of remove, or {@code null}.
      * @param  remove    {@code true} for removing the resource, or {@code 
false} for checking only.
      * @return the item wrapping the resource, or {@code null} if the resource 
has not been found in the roots.
@@ -400,8 +430,8 @@ public class ResourceTree extends TreeView<Resource> {
              */
             if (remove) {
                 final ObservableList<TreeItem<Resource>> items = 
getSelectionModel().getSelectedItems();
-                for (int i=items.size(); --i >= 0;) {
-                    if (((ResourceItem) items.get(i)).contains(resource)) {
+                for (int i = items.size(); --i >= 0;) {
+                    if (ResourceItem.isWrapperOf(items.get(i), resource)) {
                         getSelectionModel().clearSelection(i);
                     }
                 }
@@ -422,7 +452,7 @@ public class ResourceTree extends TreeView<Resource> {
                         return item;
                     }
                     if (root instanceof RootResource) {
-                        return ((RootResource) root).contains(resource, 
remove);
+                        return ((RootResource) root).findOrRemove(resource, 
remove);
                     }
                 }
             }
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/RootResource.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/RootResource.java
index 43466ea1de..0d92751c98 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/RootResource.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/RootResource.java
@@ -27,13 +27,15 @@ import org.apache.sis.storage.Resource;
 
 /**
  * The root pseudo-resource for allowing the tree to contain more than one 
resource.
- * This root node should be hidden in the {@link ResourceTree}.
+ * This is created only if needed, in which case this root node should be 
hidden in the {@link ResourceTree}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
 final class RootResource implements Aggregate {
     /**
      * The children to expose as an unmodifiable list of components.
+     * The elements of this list should be {@link ResourceItem} instances,
+     * but this class is tolerant to other classes.
      */
     private final List<TreeItem<Resource>> components;
 
@@ -41,8 +43,8 @@ final class RootResource implements Aggregate {
      * Creates a new aggregate which is going to be wrapped in the given node.
      * Caller shall invoke {@code group.setValue(root)} after this constructor.
      *
-     * @param  group     the new tree root which will contain "real" resources.
-     * @param  previous  the previous root, to be added in the new group.
+     * @param  group     the new tree root which will contain the actual 
resources.
+     * @param  previous  the previous root to be added in the new group, or 
{@code null} if none.
      */
     RootResource(final TreeItem<Resource> group, final TreeItem<Resource> 
previous) {
         components = group.getChildren();
@@ -59,10 +61,10 @@ final class RootResource implements Aggregate {
      * @param  remove    whether to remove the resource if found.
      * @return the resource wrapper, or {@code null} if not found.
      */
-    TreeItem<Resource> contains(final Resource resource, final boolean remove) 
{
-        for (int i=components.size(); --i >= 0;) {
+    final TreeItem<Resource> findOrRemove(final Resource resource, final 
boolean remove) {
+        for (int i = components.size(); --i >= 0;) {
             final TreeItem<Resource> item = components.get(i);
-            if (((ResourceItem) item).contains(resource)) {
+            if (ResourceItem.isWrapperOf(item, resource)) {
                 return remove ? components.remove(i) : item;
             }
         }
@@ -78,18 +80,16 @@ final class RootResource implements Aggregate {
      *
      * @see ResourceTree#addResource(Resource)
      */
-    boolean add(final Resource resource) {
-        for (int i = components.size(); --i >= 0;) {
-            if (((ResourceItem) components.get(i)).contains(resource)) {
-                return false;
-            }
+    public boolean add(final Resource resource) {
+        if (findOrRemove(resource, false) != null) {
+            return false;
         }
         return components.add(new ResourceItem(resource));
     }
 
     /**
      * Returns a read-only view of the components. This method is not used 
directly by {@link ResourceTree}
-     * but is defined in case a user invoke {@link 
ResourceTree#getResource()}. For this reason, it is not
+     * but is defined in case a user invokes {@link 
ResourceTree#getResource()}. For this reason, it is not
      * worth to cache the list created in this method.
      */
     @Override
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java
index a631c04084..8e2a3317ce 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java
@@ -321,6 +321,7 @@ public final class DataStoreOpener extends Task<DataStore> {
      * @return {@code true} if the value has been removed from the cache, or 
{@code false}
      *         if it has not been found. Note that the data store is closed in 
all cases.
      */
+    @SuppressWarnings("UseSpecificCatch")
     public static boolean removeAndClose(final DataStore toClose, final Node 
owner) {
         /*
          * A simpler code would be as below, but cannot be used at this time 
because our
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/GUIUtilities.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/GUIUtilities.java
index b0a1821567..f92f956723 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/GUIUtilities.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/GUIUtilities.java
@@ -40,7 +40,6 @@ import org.apache.sis.referencing.internal.shared.Formulas;
 import org.apache.sis.measure.Quantities;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.Localized;
-import org.apache.sis.util.Workaround;
 
 
 /**
@@ -193,21 +192,6 @@ walk:   for (final T search : path) {
         }
     }
 
-    /**
-     * 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);
-    }
-
     /**
      * Sets the selected value or {@code target} to the same item as the 
selected item of {@code source}.
      *


Reply via email to