You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/09/12 15:48:01 UTC

[sis] 03/03: Add a menu item for showing the aggregated view of the content of a folder.

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 <ma...@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.