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}.
*