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.