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
commit 657f53891b23bb60cc965b76cb81add2b4756dab Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Sep 12 17:43:48 2022 +0200 Add a menu item for showing the aggregated view of the content of a folder. --- .../org/apache/sis/gui/dataset/ResourceCell.java | 79 ++++++--- .../org/apache/sis/gui/dataset/ResourceItem.java | 185 ++++++++++++++++++++- .../org/apache/sis/gui/dataset/ResourceTree.java | 12 +- .../org/apache/sis/gui/dataset/RootResource.java | 7 +- .../org/apache/sis/gui/dataset/TreeViewType.java | 40 +++++ .../org/apache/sis/internal/gui/Resources.java | 29 +++- .../apache/sis/internal/gui/Resources.properties | 2 + .../sis/internal/gui/Resources_fr.properties | 2 + 8 files changed, 317 insertions(+), 39 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 index 109bd88397..3d7f4d4fb9 100644 --- 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 @@ -21,6 +21,7 @@ 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.paint.Color; @@ -55,21 +56,14 @@ import org.apache.sis.util.resources.Vocabulary; */ final class ResourceCell extends TreeCell<Resource> { /** - * Creates a new cell with initially no data. + * The type of view (original resource, aggregated resources, etc.) shown in this node. */ - ResourceCell() { - } + private TreeViewType viewType; /** - * 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. + * Creates a new cell with initially no data. */ - 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; + ResourceCell() { } /** @@ -121,7 +115,8 @@ final class ResourceCell extends TreeCell<Resource> { text = Vocabulary.getResources(tree.locale).getString(Vocabulary.Keys.Unnamed); } else { // More serious error (no resource), show exception message. - text = string(error, tree.locale); + text = Strings.trimOrNull(Exceptions.getLocalizedMessage(error, tree.locale)); + if (text == null) text = Classes.getShortClassName(error); } item.label = text; } @@ -137,25 +132,14 @@ final class ResourceCell extends TreeCell<Resource> { }); } /* - * 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. + * Following block is for the contextual menu. In current version, + * we provide menu only for "root" resources (usually data stores). */ 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. + * "Aggregated view" should be enabled only on supported resources. */ Object path; try { @@ -164,9 +148,31 @@ final class ResourceCell extends TreeCell<Resource> { path = null; ResourceTree.unexpectedException("updateItem", e); } + final boolean aggregatable = item.isViewSelectable(resource, TreeViewType.AGGREGATION); + /* + * Create (if not already done) and configure contextual menu using above information. + */ + 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[AGGREGATED] = localized.menu(Resources.Keys.AggregatedView, false, (p,o,n) -> { + setView(n ? TreeViewType.AGGREGATION : TreeViewType.SOURCE); + }); + items[CLOSE] = localized.menu(Resources.Keys.Close, (e) -> { + ((ResourceTree) getTreeView()).removeAndClose(getItem()); + }); + menu.getItems().setAll(items); + } 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); + final CheckMenuItem aggregated = (CheckMenuItem) items.get(AGGREGATED); + aggregated.setDisable(!aggregatable); + aggregated.setSelected(aggregatable && item.isView(TreeViewType.AGGREGATION)); } } setText(text); @@ -179,5 +185,24 @@ final class ResourceCell extends TreeCell<Resource> { * 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; + private static final int COPY_PATH = 0, OPEN_FOLDER = 1, AGGREGATED = 2, CLOSE = 3; + + /** + * Sets the view of the resource to show in this node. + * For example instead of showing the components as given by the data store, + * 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/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 index 1b6151ff46..f12d78fd6b 100644 --- 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 @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.util.Locale; import java.util.List; import java.util.ArrayList; +import java.util.EnumMap; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.collections.ObservableList; @@ -27,6 +28,7 @@ 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.storage.folder.UnstructuredAggregate; import org.apache.sis.internal.gui.DataStoreOpener; import org.apache.sis.internal.gui.BackgroundThreads; import org.apache.sis.internal.gui.GUIUtilities; @@ -35,8 +37,8 @@ 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}. + * 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}. * * @author Martin Desruisseaux (Geomatys) * @version 1.3 @@ -105,7 +107,7 @@ final class ResourceItem extends TreeItem<Resource> { /** * Creates an item for a resource that we failed to load. */ - ResourceItem(final Throwable exception) { + private ResourceItem(final Throwable exception) { isLeaf = true; error = exception; } @@ -118,13 +120,13 @@ final class ResourceItem extends TreeItem<Resource> { ResourceItem(final Resource resource) { super(resource); isLoading = true; // Means that the label still need to be fetched. - isLeaf = !(resource instanceof Aggregate); + 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}. + * 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. */ @@ -152,7 +154,7 @@ final class ResourceItem extends TreeItem<Resource> { } /** Invoked in JavaFX thread after the label has been fetched. */ - public void run() { + @Override public void run() { isLoading = false; label = result; error = failure; @@ -232,6 +234,8 @@ final class ResourceItem extends TreeItem<Resource> { /** * Invoked in JavaFX thread if children can not be loaded. + * This method replaces all children (which are unknown) by + * a single node which represents a failure to load the data. */ @Override @SuppressWarnings("unchecked") @@ -239,4 +243,173 @@ final class ResourceItem extends TreeItem<Resource> { ResourceItem.super.getChildren().setAll(new ResourceItem(getException())); } } + + + + + // ┌──────────────────────────────────────────────────────────────────────────────────────────┐ + // │ Management of different Views of the resoure (for example aggregations of folder conent) │ + // └──────────────────────────────────────────────────────────────────────────────────────────┘ + + /** + * If derived resources (aggregation, etc.) are created, the derived resource for each view. + * 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; + + /** + * Returns the resource which is the source of this item. + */ + final Resource getSource() { + return (views != null ? views.get(TreeViewType.SOURCE) : this).getValue(); + } + + /** + * Returns {@code true} if the value, or the value of one of the views, is the given 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) { + return true; + } + if (views != null) { + for (final ResourceItem view : views.values()) { + if (view.getValue() == resource) { + return true; + } + } + } + return false; + } + + /** + * Returns whether this item is for the specified view. + * This is used for deciding whether the corresponding menu item should be checked. + * + * @param type the view to test. + * @return whether this item is for the specified view. + */ + final boolean isView(final TreeViewType type) { + return (views != null) && views.get(type) == this; + } + + /** + * Returns whether the specified type of view can be used with the given resource. + * + * @param resource the resource on which different types of views may apply. + * @param type the desired type of view. + * @return whether the specified type of view can be used. + */ + final boolean isViewSelectable(final Resource resource, final TreeViewType type) { + if (views != null && views.containsKey(type)) { + return true; + } + if (getParent() != null) { // Views can be changed only if a parent exists. + switch (type) { + case AGGREGATION: return (resource instanceof UnstructuredAggregate); + // More views may be added in the future. + } + } + return false; + } + + /** + * Replaces this resource item by the specified view. + * The replacement is performed in the list of children of the parent. + * + * @param view the view to select as the active view. + */ + private void selectView(final ResourceItem view) { + final TreeItem<Resource> parent = getParent(); + final List<TreeItem<Resource>> siblings; + if (parent != null) { + siblings = parent.getChildren(); + final int i = siblings.indexOf(this); + if (i >= 0) { + siblings.set(i, view); + return; + } + // Should never happen, otherwise the `parent` information would be wrong. + } else { + siblings = super.getChildren(); + } + /* + * Following fallback should never happen. If it happen anyway, add the view as a sibling + * for avoiding the complete lost of the resource. It is possible only if a parent exists. + * A parent may not exist if the resource was declared by `ResourceTree.setResource(…)`, + * in which case we do not want to change the resource specified by user. + */ + 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. + * + * @param cell the cell which is requesting a view. + * @param type the type of view to show. + * @param locale the locale to use for fetching resource label. + */ + final void setView(final ResourceCell cell, final TreeViewType type, final Locale locale) { + if (views == null) { + views = new EnumMap<>(TreeViewType.class); + views.put(TreeViewType.SOURCE, this); + } + final ResourceItem existing = views.get(type); + if (existing != null) { + selectView(existing); + return; + } + final Resource resource = getSource(); + final ResourceItem loading = new ResourceItem(); + setNewView(null, type, loading); + BackgroundThreads.execute(new Task<ResourceItem>() { + /** Fetch in a background thread the view selected by user. */ + @Override protected ResourceItem call() throws DataStoreException { + Resource result = resource; + switch (type) { + case AGGREGATION: { + if (resource instanceof UnstructuredAggregate) { + result = ((UnstructuredAggregate) resource).getStructuredView(); + } + break; + } + // More cases may be added in the future. + } + final ResourceItem item = new ResourceItem(result); + item.label = DataStoreOpener.findLabel(resource, locale, false); + item.isLoading = false; + return item; + } + + /** Invoked in JavaFX thread after the requested view has been obtained. */ + @Override protected void succeeded() { + loading.setNewView(cell, type, getValue()); + } + + /** Invoked in JavaFX thread if an exception occurred while fetching the view. */ + @Override protected void failed() { + loading.setNewView(cell, type, 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 3cb14838ec..afb32f8670 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 @@ -256,6 +256,9 @@ public class ResourceTree extends TreeView<Resource> { * Adds the given store as a resource, then notifies {@link #onResourceLoaded} * handler that a resource at the given path has been loaded. * This method is invoked from JavaFX thread. + * + * @param store the data store which has been loaded. + * @param source the user-supplied object which was the input of the store. */ private void addLoadedResource(final DataStore store, final Object source) { final boolean added = addResource(store); @@ -351,9 +354,12 @@ public class ResourceTree extends TreeView<Resource> { * @see #addResource(Resource) * @see ResourceExplorer#removeAndClose(Resource) */ - public void removeAndClose(final Resource resource) { + public void removeAndClose(Resource resource) { final TreeItem<Resource> item = findOrRemove(resource, true); - if (item != null && resource instanceof DataStore) { + if (item instanceof ResourceItem) { + resource = ((ResourceItem) item).getSource(); + } + if (resource instanceof DataStore) { final DataStore store = (DataStore) resource; DataStoreOpener.removeAndClose(store, this); final EventHandler<ResourceEvent> handler = onResourceClosed.get(); @@ -396,7 +402,7 @@ public class ResourceTree extends TreeView<Resource> { if (remove) { final ObservableList<TreeItem<Resource>> items = getSelectionModel().getSelectedItems(); for (int i=items.size(); --i >= 0;) { - if (items.get(i).getValue() == resource) { + if (((ResourceItem) items.get(i)).contains(resource)) { getSelectionModel().clearSelection(i); } } 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 index 86942977e5..fa34e50f7a 100644 --- 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 @@ -69,7 +69,7 @@ final class RootResource implements Aggregate { 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) { + if (((ResourceItem) item).contains(resource)) { return remove ? components.remove(i) : item; } } @@ -78,13 +78,16 @@ final class RootResource implements Aggregate { /** * Adds the given resource if not already present. + * This is invoked when new resources are opened and listed in {@link ResourceTree}. * * @param resource the resource to add. * @return whether the given resource has been added. + * + * @see ResourceTree#addResource(Resource) */ boolean add(final Resource resource) { for (int i = components.size(); --i >= 0;) { - if (components.get(i).getValue() == resource) { + if (((ResourceItem) components.get(i)).contains(resource)) { return false; } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/TreeViewType.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/TreeViewType.java new file mode 100644 index 0000000000..075ac3cba1 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/TreeViewType.java @@ -0,0 +1,40 @@ +/* + * 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 org.apache.sis.internal.storage.folder.UnstructuredAggregate; + + +/** + * The different views (aggregation, etc.) which may be associated to a resource item. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +enum TreeViewType { + /** + * The original resource. Associated value shall never be {@code null}. + */ + SOURCE, + + /** + * The result of {@link UnstructuredAggregate#getStructuredView()}. + */ + AGGREGATION +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java index 1b83d23c2a..ac56319c39 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java @@ -22,6 +22,8 @@ import java.util.MissingResourceException; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.control.MenuItem; +import javafx.scene.control.CheckMenuItem; +import javafx.beans.value.ChangeListener; import org.apache.sis.util.resources.KeyConstants; import org.apache.sis.util.resources.IndexedResourceBundle; @@ -32,7 +34,7 @@ import org.apache.sis.util.resources.IndexedResourceBundle; * all modules in the Apache SIS project, see {@link org.apache.sis.util.resources} package. * * @author Johann Sorel (Geomatys) - * @version 1.1 + * @version 1.3 * @since 1.1 * @module */ @@ -65,6 +67,11 @@ public final class Resources extends IndexedResourceBundle { */ public static final short AccessedRegions = 65; + /** + * Aggregated view + */ + public static final short AggregatedView = 75; + /** * All files */ @@ -80,6 +87,11 @@ public final class Resources extends IndexedResourceBundle { */ public static final short AzimuthalEquidistant = 42; + /** + * Can not create an aggregated view of “{0}”. + */ + public static final short CanNotAggregate_1 = 76; + /** * Can not close “{0}”. Data may be lost. */ @@ -543,4 +555,19 @@ public final class Resources extends IndexedResourceBundle { item.setOnAction(onAction); return item; } + + /** + * Creates a new check menu item with a localized text specified by the given key. + * + * @param key the key for the text of the menu item. + * @param selected initial state of the check menu item. + * @param onAction action to execute when the menu is selected or unselected. + * @return the menu item with the specified text and action. + */ + public CheckMenuItem menu(final short key, final boolean selected, final ChangeListener<Boolean> onAction) { + final CheckMenuItem item = new CheckMenuItem(getString(key)); + item.setSelected(selected); + item.selectedProperty().addListener(onAction); + return item; + } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties index 3081492527..75cee6b09e 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties @@ -22,9 +22,11 @@ About = About\u2026 AccessedRegions = Accessed regions +AggregatedView = Aggregated view AllFiles = All files Along_1 = Along {0} AzimuthalEquidistant = Azimuthal equidistant +CanNotAggregate_1 = Can not create an aggregated view of \u201c{0}\u201d. CanNotFetchTile_2 = Can not fetch tile ({0}, {1}). CanNotReadFile_1 = Can not open \u201c{0}\u201d. CanNotClose_1 = Can not close \u201c{0}\u201d. Data may be lost. diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties index bbc4e0f7af..ee23e38e8c 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties @@ -27,9 +27,11 @@ About = \u00c0 propos de\u2026 AccessedRegions = R\u00e9gions acc\u00e9d\u00e9es +AggregatedView = Vue agr\u00e9g\u00e9e AllFiles = Tous les fichiers Along_1 = Selon {0} AzimuthalEquidistant = Azimutal \u00e9quidistant +CanNotAggregate_1 = Ne peut pas cr\u00e9er une vue agr\u00e9g\u00e9e de \u00ab\u202f{0}\u202f\u00bb. CanNotFetchTile_2 = Ne peut pas obtenir la tuile ({0}, {1}). CanNotReadFile_1 = Ne peut pas ouvrir \u00ab\u202f{0}\u202f\u00bb. CanNotClose_1 = Ne peut pas fermer \u00ab\u202f{0}\u202f\u00bb. Il pourrait y avoir une perte de donn\u00e9es.