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 f0a2456d6f Move the static nested classes of `ResourceTree` as package-private top-level classes. Those classes are becoming bigger as new actions (e.g. context menu items) are added. f0a2456d6f is described below commit f0a2456d6fa1807240878133fea5e77345fb13f0 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Sep 10 23:59:47 2022 +0200 Move the static nested classes of `ResourceTree` as package-private top-level classes. Those classes are becoming bigger as new actions (e.g. context menu items) are added. --- .../org/apache/sis/gui/dataset/ResourceCell.java | 183 ++++++++ .../org/apache/sis/gui/dataset/ResourceItem.java | 242 ++++++++++ .../org/apache/sis/gui/dataset/ResourceTree.java | 516 ++------------------- .../org/apache/sis/gui/dataset/RootResource.java | 132 ++++++ 4 files changed, 585 insertions(+), 488 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java new file mode 100644 index 0000000000..109bd88397 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceCell.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.paint.Color; +import org.apache.sis.storage.Resource; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.internal.storage.URIDataStore; +import org.apache.sis.internal.storage.io.IOUtilities; +import org.apache.sis.internal.gui.ExceptionReporter; +import org.apache.sis.internal.gui.DataStoreOpener; +import org.apache.sis.internal.gui.Resources; +import org.apache.sis.internal.gui.Styles; +import org.apache.sis.internal.util.Strings; +import org.apache.sis.util.Classes; +import org.apache.sis.util.Exceptions; +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. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * + * @see ResourceItem + * + * @since 1.3 + * @module + */ +final class ResourceCell extends TreeCell<Resource> { + /** + * Creates a new cell with initially no data. + */ + ResourceCell() { + } + + /** + * Returns a localized (if possible) string representation of the given exception. + * This method returns the message if one exists, or the exception class name otherwise. + */ + 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); + } + return text; + } + + /** + * 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. + * + * @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) { + 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 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 (text == null) { + text = item.label = tree.localized().getString(Resources.Keys.Loading); + if (resource != null) { + tree.fetchLabel(item.new Completer(resource)); // Start a background thread. + } + } + } 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 (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; + } + 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); + }); + } + /* + * 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) != null) { + menu = getContextMenu(); + if (menu == null) { + menu = new ContextMenu(); + final Resources localized = tree.localized(); + final MenuItem[] items = new MenuItem[CLOSE + 1]; + items[COPY_PATH] = localized.menu(Resources.Keys.CopyFilePath, new PathAction(this, false)); + items[OPEN_FOLDER] = localized.menu(Resources.Keys.OpenContainingFolder, new PathAction(this, true)); + items[CLOSE] = localized.menu(Resources.Keys.Close, (e) -> { + ((ResourceTree) getTreeView()).removeAndClose(getItem()); + }); + menu.getItems().setAll(items); + } + /* + * "Copy file path" menu item should be enabled only if we can + * get some kind of file path or URI from the specified resource. + */ + Object path; + try { + path = URIDataStore.location(resource); + } catch (DataStoreException e) { + path = null; + ResourceTree.unexpectedException("updateItem", e); + } + final ObservableList<MenuItem> items = menu.getItems(); + items.get(COPY_PATH).setDisable(!IOUtilities.isKindOfPath(path)); + items.get(OPEN_FOLDER).setDisable(PathAction.isBrowseDisabled || IOUtilities.toFile(path) == 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. + */ + private static final int COPY_PATH = 0, OPEN_FOLDER = 1, CLOSE = 2; +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java new file mode 100644 index 0000000000..1b6151ff46 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceItem.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui.dataset; + +import java.nio.file.Path; +import java.util.Locale; +import java.util.List; +import java.util.ArrayList; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; +import org.apache.sis.storage.Resource; +import org.apache.sis.storage.Aggregate; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.internal.gui.DataStoreOpener; +import org.apache.sis.internal.gui.BackgroundThreads; +import org.apache.sis.internal.gui.GUIUtilities; +import org.apache.sis.internal.gui.LogHandler; + + +/** + * 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}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * + * @see Cell + * + * @since 1.3 + * @module + */ +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. + */ + Path path; + + /** + * The text of this node, computed and cached when first needed. Computation is done by invoking + * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a background thread. + * + * @see ResourceTree#fetchLabel(ResourceItem.Completer) + */ + 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. + */ + private final 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. + * + * @todo Register {@link org.apache.sis.storage.event.StoreListener} and reset + * this flag to {@code false} if the resource content or structure changed. + */ + 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. + */ + ResourceItem() { + isLeaf = true; + isLoading = true; + } + + /** + * Creates an item for a resource that we failed to load. + */ + ResourceItem(final Throwable exception) { + isLeaf = true; + error = exception; + } + + /** + * Creates a new node for the given resource. + * + * @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); + 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}. + */ + final class Completer implements Runnable { + /** The resource for which to fetch a label. */ + private final Resource resource; + + /** 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) { + this.resource = resource; + } + + /** Invoked in a background thread for fetching the label. */ + final void fetch(final Locale locale) { + try { + result = DataStoreOpener.findLabel(resource, locale, false); + } catch (Throwable e) { + failure = e; + } + Platform.runLater(this); + } + + /** Invoked in JavaFX thread after the label has been fetched. */ + public void run() { + isLoading = false; + label = result; + error = failure; + GUIUtilities.forceCellUpdate(ResourceItem.this); + } + } + + /** + * Returns whether the resource can not have children. + */ + @Override + public boolean isLeaf() { + return isLeaf; + } + + /** + * Returns the items for all sub-resources contained in this resource. + * The list is empty if the resource is not an aggregate. + */ + @Override + public ObservableList<TreeItem<Resource>> getChildren() { + final ObservableList<TreeItem<Resource>> children = super.getChildren(); + if (!isChildrenKnown) { + isChildrenKnown = true; // Set first for avoiding to repeat in case of failure. + final Resource resource = getValue(); + if (resource instanceof Aggregate) { + BackgroundThreads.execute(new GetChildren((Aggregate) resource)); + children.add(new ResourceItem()); + } + } + return children; + } + + /** + * The task to execute in a background thread for fetching the children. + */ + private final class GetChildren extends Task<List<TreeItem<Resource>>> { + /** + * The aggregate from which to get the children. + */ + private final Aggregate resource; + + /** + * Creates a new background task for fetching the children from the given resource. + */ + GetChildren(final Aggregate resource) { + this.resource = resource; + } + + /** + * Invoked in a background thread for fetching the children of the resource + * specified at construction time. + */ + @Override + protected List<TreeItem<Resource>> call() throws DataStoreException { + final List<TreeItem<Resource>> items = new ArrayList<>(); + final Long id = LogHandler.loadingStart(resource); + try { + for (final Resource component : resource.components()) { + items.add(new ResourceItem(component)); + } + } finally { + LogHandler.loadingStop(id); + } + return items; + } + + /** + * 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() { + ResourceItem.super.getChildren().setAll(getValue()); + } + + /** + * Invoked in JavaFX thread if children can not be loaded. + */ + @Override + @SuppressWarnings("unchecked") + protected void failed() { + ResourceItem.super.getChildren().setAll(new ResourceItem(getException())); + } + } +} 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 bab1d75692..3cb14838ec 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 @@ -21,52 +21,31 @@ import java.nio.file.Path; import java.net.URL; import java.net.MalformedURLException; import java.nio.file.FileSystemNotFoundException; -import java.util.AbstractList; import java.util.Locale; import java.util.Queue; import java.util.List; -import java.util.ArrayList; import java.util.LinkedList; import java.util.Collection; -import java.util.Optional; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.collections.ObservableList; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.EventHandler; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.MenuItem; -import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; -import javafx.scene.paint.Color; -import org.opengis.util.GenericName; -import org.opengis.metadata.Metadata; -import org.apache.sis.util.resources.Vocabulary; -import org.apache.sis.util.Exceptions; -import org.apache.sis.util.Classes; import org.apache.sis.storage.Resource; import org.apache.sis.storage.Aggregate; import org.apache.sis.storage.DataStore; -import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.event.StoreEvent; -import org.apache.sis.storage.event.StoreListener; -import org.apache.sis.internal.storage.URIDataStore; import org.apache.sis.internal.storage.io.IOUtilities; import org.apache.sis.internal.gui.DataStoreOpener; 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; import org.apache.sis.internal.system.Modules; -import org.apache.sis.internal.util.Strings; import org.apache.sis.storage.DataStoreProvider; import org.apache.sis.util.logging.Logging; @@ -93,7 +72,7 @@ import static java.util.logging.Logger.getLogger; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.1 * @module */ @@ -133,9 +112,9 @@ public class ResourceTree extends TreeView<Resource> { * 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. * - * @see #fetchLabel(Item.Completer) + * @see #fetchLabel(ResourceItem.Completer) */ - private final Queue<Item.Completer> pendingItems; + private final Queue<ResourceItem.Completer> pendingItems; /** * Creates a new tree of resources with initially no resource to show. @@ -144,7 +123,7 @@ public class ResourceTree extends TreeView<Resource> { public ResourceTree() { locale = Locale.getDefault(); pendingItems = new LinkedList<>(); - setCellFactory((v) -> new Cell()); + setCellFactory((v) -> new ResourceCell()); setOnDragOver(ResourceTree::onDragOver); setOnDragDropped(this::onDragDropped); onResourceLoaded = new SimpleObjectProperty<>(this, "onResourceLoaded"); @@ -167,7 +146,7 @@ public class ResourceTree extends TreeView<Resource> { */ public Resource getResource() { final TreeItem<Resource> item = getRoot(); - return item == null ? null : item.getValue(); + return (item == null) ? null : item.getValue(); } /** @@ -186,8 +165,8 @@ public class ResourceTree extends TreeView<Resource> { * @see #removeAndClose(Resource) */ public void setResource(final Resource resource) { - setRoot(resource == null ? null : new Item(resource)); - setShowRoot(!(resource instanceof Root)); + setRoot(resource == null ? null : new ResourceItem(resource)); + setShowRoot(!(resource instanceof RootResource)); } /** @@ -210,27 +189,27 @@ public class ResourceTree extends TreeView<Resource> { if (resource == null) { return false; } - Root addTo = null; + RootResource addTo = null; final TreeItem<Resource> item = getRoot(); if (item != null) { final Resource root = item.getValue(); if (root == resource) { return false; } - if (root instanceof Root) { - addTo = (Root) root; + if (root instanceof RootResource) { + addTo = (RootResource) root; } } /* - * We create the `Root` pseudo-resource even if there is only one resource. - * A previous version created `Root` only if there was two or more ressources, + * We create the `RootResource` pseudo-resource even if there is only one resource. + * A previous version created `RootResource` only if there was two or more ressources, * but it was causing confusing events when the second resource was added. */ if (addTo == null) { final TreeItem<Resource> group = new TreeItem<>(); setShowRoot(false); setRoot(group); // Also detach `item` from the TreeView root. - addTo = new Root(group, item); // Pseudo-resource for a group of data stores. + addTo = new RootResource(group, item); // Pseudo-resource for a group of data stores. group.setValue(addTo); } return addTo.add(resource); @@ -296,7 +275,7 @@ public class ResourceTree extends TreeView<Resource> { * wrapping of resources. */ if (added) { - ((Item) findOrRemove(store, false)).path = path; + ((ResourceItem) findOrRemove(store, false)).path = path; } handler.handle(new ResourceEvent(this, path, ResourceEvent.LOADED)); } @@ -358,7 +337,7 @@ public class ResourceTree extends TreeView<Resource> { * * <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. + * Children of {@link Aggregate} resource are not scanned. * If the given resource can not be removed, then this method does nothing.</p> * * <h4>Notifications</h4> @@ -380,8 +359,8 @@ public class ResourceTree extends TreeView<Resource> { final EventHandler<ResourceEvent> handler = onResourceClosed.get(); if (handler != null) { Path path = null; - if (item instanceof Item) { - path = ((Item) item).path; + if (item instanceof ResourceItem) { + path = ((ResourceItem) item).path; } if (path == null) try { path = store.getOpenParameters() @@ -400,12 +379,14 @@ public class ResourceTree extends TreeView<Resource> { /** * Verifies if the given resource is one of the roots, and optionally removes it. + * If {@code remove} is {@code true}, then it is caller's responsibility to close + * the resource. * * @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. */ - private TreeItem<Resource> findOrRemove(final Resource resource, final boolean remove) { + final TreeItem<Resource> findOrRemove(final Resource resource, final boolean remove) { assert Platform.isFxApplicationThread(); if (resource != null) { /* @@ -422,6 +403,8 @@ public class ResourceTree extends TreeView<Resource> { } /* * Search for the resource from the root, and optionally remove it. + * Intentionally use identity comparison, not `Object.equals(…)` + * (should be consistent in whole `ResourceTree` implementation). */ final TreeItem<Resource> item = getRoot(); if (item != null) { @@ -433,8 +416,8 @@ public class ResourceTree extends TreeView<Resource> { } return item; } - if (root instanceof Root) { - return ((Root) root).contains(resource, remove); + if (root instanceof RootResource) { + return ((RootResource) root).contains(resource, remove); } } } @@ -443,10 +426,10 @@ public class ResourceTree extends TreeView<Resource> { } /** - * Updates {@link Item#label} with the resource label fetched in background thread. - * Caller should invoke this method only if {@link Item#isLoading} is {@code true}. + * Updates {@link ResourceItem#label} with the resource label fetched in background thread. + * Caller should invoke this method only if {@link ResourceItem#isLoading} is {@code true}. */ - private void fetchLabel(final Item.Completer item) { + final void fetchLabel(final ResourceItem.Completer item) { final boolean isEmpty; synchronized (pendingItems) { // The two operations below must be atomic (this is why we do not use ConcurrentLinkedQueue). @@ -457,7 +440,7 @@ public class ResourceTree extends TreeView<Resource> { // Not a problem if 2 tasks are launched in parallel. BackgroundThreads.execute(() -> { for (;;) { - final Item.Completer c; + final ResourceItem.Completer c; synchronized (pendingItems) { c = pendingItems.poll(); } @@ -475,18 +458,6 @@ public class ResourceTree extends TreeView<Resource> { return Resources.forLocale(locale); } - /** - * 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 static String string(final Throwable failure, final Locale locale) { - String text = Strings.trimOrNull(Exceptions.getLocalizedMessage(failure, locale)); - if (text == null) { - text = Classes.getShortClassName(failure); - } - return text; - } - /** * Reports an ignorable exception in the given method. */ @@ -500,435 +471,4 @@ public class ResourceTree extends TreeView<Resource> { static void unexpectedException(final String method, final Exception e) { Logging.unexpectedException(getLogger(Modules.APPLICATION), ResourceTree.class, method, e); } - - - - - /** - * 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 DataStoreOpener#findLabel(Resource, Locale, boolean)} in a background thread. - * The same call may be recycled many times for different {@link Item} data. - * - * @see Item - */ - private static final class Cell extends TreeCell<Resource> { - /** - * Creates a new cell with initially no data. - */ - Cell() { - } - - /** - * 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. - * - * @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) { - 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 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 (text == null) { - text = item.label = tree.localized().getString(Resources.Keys.Loading); - if (resource != null) { - tree.fetchLabel(item.new Completer(resource)); // Start a background thread. - } - } - } 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 (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; - } - 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); - }); - } - /* - * 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) != null) { - menu = getContextMenu(); - if (menu == null) { - menu = new ContextMenu(); - final Resources localized = tree.localized(); - final MenuItem[] items = new MenuItem[CLOSE + 1]; - items[COPY_PATH] = localized.menu(Resources.Keys.CopyFilePath, new PathAction(this, false)); - items[OPEN_FOLDER] = localized.menu(Resources.Keys.OpenContainingFolder, new PathAction(this, true)); - items[CLOSE] = localized.menu(Resources.Keys.Close, (e) -> { - ((ResourceTree) getTreeView()).removeAndClose(getItem()); - }); - menu.getItems().setAll(items); - } - /* - * "Copy file path" menu item should be enabled only if we can - * get some kind of file path or URI from the specified resource. - */ - Object path; - try { - path = URIDataStore.location(resource); - } catch (DataStoreException e) { - path = null; - unexpectedException("updateItem", e); - } - menu.getItems().get(COPY_PATH).setDisable(!IOUtilities.isKindOfPath(path)); - menu.getItems().get(OPEN_FOLDER).setDisable(PathAction.isBrowseDisabled || IOUtilities.toFile(path) == 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. - */ - private static final int COPY_PATH = 0, OPEN_FOLDER = 1, CLOSE = 2; - } - - - - - /** - * 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 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. - */ - Path path; - - /** - * The text of this node, computed and cached when first needed. Computation is done by invoking - * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a background thread. - * - * @see #fetchLabel(Item.Completer) - */ - 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. - */ - private final 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. - * - * @todo Register {@link org.apache.sis.storage.event.StoreListener} and reset - * this flag to {@code false} if the resource content or structure changed. - */ - 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}. - */ - final class Completer implements Runnable { - /** The resource for which to fetch a label. */ - private final Resource resource; - - /** 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) { - this.resource = resource; - } - - /** Invoked in a background thread for fetching the label. */ - final void fetch(final Locale locale) { - try { - result = DataStoreOpener.findLabel(resource, locale, false); - } catch (Throwable e) { - failure = e; - } - Platform.runLater(this); - } - - /** Invoked in JavaFX thread after the label has been fetched. */ - public void run() { - isLoading = false; - label = result; - error = failure; - GUIUtilities.forceCellUpdate(Item.this); - } - } - - /** - * Returns whether the resource can not have children. - */ - @Override - public boolean isLeaf() { - return isLeaf; - } - - /** - * Returns the items for all sub-resources contained in this resource. - * The list is empty if the resource is not an aggregate. - */ - @Override - public ObservableList<TreeItem<Resource>> getChildren() { - final ObservableList<TreeItem<Resource>> children = super.getChildren(); - if (!isChildrenKnown) { - isChildrenKnown = true; // Set first for avoiding to repeat in case of failure. - final Resource resource = getValue(); - if (resource instanceof Aggregate) { - BackgroundThreads.execute(new GetChildren((Aggregate) resource)); - children.add(new Item()); - } - } - return children; - } - - /** - * The task to execute in a background thread for fetching the children. - */ - private final class GetChildren extends Task<List<TreeItem<Resource>>> { - /** - * The aggregate from which to get the children. - */ - private final Aggregate resource; - - /** - * Creates a new background task for fetching the children from the given resource. - */ - GetChildren(final Aggregate resource) { - this.resource = resource; - } - - /** - * Invoked in a background thread for fetching the children of the resource - * specified at construction time. - */ - @Override - protected List<TreeItem<Resource>> call() throws DataStoreException { - final List<TreeItem<Resource>> items = new ArrayList<>(); - final Long id = LogHandler.loadingStart(resource); - try { - for (final Resource component : resource.components()) { - items.add(new Item(component)); - } - } finally { - LogHandler.loadingStop(id); - } - return items; - } - - /** - * 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() { - Item.super.getChildren().setAll(getValue()); - } - - /** - * Invoked in JavaFX thread if children can not be loaded. - */ - @Override - @SuppressWarnings("unchecked") - protected void failed() { - Item.super.getChildren().setAll(new Item(getException())); - } - } - } - - - - - /** - * The root pseudo-resource for allowing the tree to contain more than one resource. - * This root node should be hidden in the {@link ResourceTree}. - */ - private static final class Root implements Aggregate { - /** - * The children to expose as an unmodifiable list of components. - */ - private final List<TreeItem<Resource>> components; - - /** - * 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. - */ - Root(final TreeItem<Resource> group, final TreeItem<Resource> previous) { - components = group.getChildren(); - if (previous != null) { - components.add(previous); - } - } - - /** - * Checks whether this root contains the given resource as a direct child. - * This method does not search recursively in sub-trees. - * - * @param resource the resource to search. - * @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> item = components.get(i); - if (item.getValue() == resource) { - return remove ? components.remove(i) : item; - } - } - return null; - } - - /** - * Adds the given resource if not already present. - * - * @param resource the resource to add. - * @return whether the given resource has been added. - */ - boolean add(final Resource resource) { - for (int i = components.size(); --i >= 0;) { - if (components.get(i).getValue() == resource) { - return false; - } - } - return components.add(new Item(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 - * worth to cache the list created in this method. - */ - @Override - public Collection<Resource> components() { - return new AbstractList<Resource>() { - @Override public int size() { - return components.size(); - } - - @Override public Resource get(final int index) { - return components.get(index).getValue(); - } - }; - } - - /** - * Returns empty optional since this resource has no identifier. - */ - @Override - public Optional<GenericName> getIdentifier() { - return Optional.empty(); - } - - /** - * Returns null since this resource has no metadata. Returning null is normally - * not allowed for this method, but {@link ResourceTree} is robust to this case. - */ - @Override - public Metadata getMetadata() { - return null; - } - - /** Ignored since this class does not emit any event. */ - @Override public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {} - @Override public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) {} - } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java new file mode 100644 index 0000000000..86942977e5 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/RootResource.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui.dataset; + +import java.util.AbstractList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import javafx.scene.control.TreeItem; +import org.apache.sis.storage.Aggregate; +import org.apache.sis.storage.Resource; +import org.apache.sis.storage.event.StoreEvent; +import org.apache.sis.storage.event.StoreListener; +import org.opengis.metadata.Metadata; +import org.opengis.util.GenericName; + + +/** + * The root pseudo-resource for allowing the tree to contain more than one resource. + * This root node should be hidden in the {@link ResourceTree}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +final class RootResource implements Aggregate { + /** + * The children to expose as an unmodifiable list of components. + */ + private final List<TreeItem<Resource>> components; + + /** + * 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. + */ + RootResource(final TreeItem<Resource> group, final TreeItem<Resource> previous) { + components = group.getChildren(); + if (previous != null) { + components.add(previous); + } + } + + /** + * Checks whether this root contains the given resource as a direct child. + * This method does not search recursively in sub-trees. + * + * @param resource the resource to search. + * @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> item = components.get(i); + if (item.getValue() == resource) { + return remove ? components.remove(i) : item; + } + } + return null; + } + + /** + * Adds the given resource if not already present. + * + * @param resource the resource to add. + * @return whether the given resource has been added. + */ + boolean add(final Resource resource) { + for (int i = components.size(); --i >= 0;) { + if (components.get(i).getValue() == resource) { + 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 + * worth to cache the list created in this method. + */ + @Override + public Collection<Resource> components() { + return new AbstractList<Resource>() { + @Override public int size() { + return components.size(); + } + + @Override public Resource get(final int index) { + return components.get(index).getValue(); + } + }; + } + + /** + * Returns empty optional since this resource has no identifier. + */ + @Override + public Optional<GenericName> getIdentifier() { + return Optional.empty(); + } + + /** + * Returns null since this resource has no metadata. Returning null is normally + * not allowed for this method, but {@link ResourceTree} is robust to this case. + */ + @Override + public Metadata getMetadata() { + return null; + } + + /** Ignored since this class does not emit any event. */ + @Override public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {} + @Override public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) {} +}