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 6224e87dca Redesign the management of multiple windows opened on the 
same resources. The intent is to allow synchronized navigations between 
different views.
6224e87dca is described below

commit 6224e87dca84232d30d409f123b6f92d0ac531ca
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jun 3 19:03:26 2022 +0200

    Redesign the management of multiple windows opened on the same resources.
    The intent is to allow synchronized navigations between different views.
---
 .../main/java/org/apache/sis/gui/DataViewer.java   |  13 +-
 .../apache/sis/gui/coverage/CoverageControls.java  |  17 +-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  99 +++++-
 .../apache/sis/gui/coverage/CoverageStyling.java   |   3 +-
 .../org/apache/sis/gui/dataset/DataWindow.java     | 116 -------
 .../apache/sis/gui/dataset/ResourceExplorer.java   | 169 ++++------
 .../org/apache/sis/gui/dataset/SelectedData.java   |  91 ------
 .../org/apache/sis/gui/dataset/WindowHandler.java  | 352 +++++++++++++++++++++
 .../org/apache/sis/gui/dataset/WindowManager.java  | 246 +++++---------
 .../org/apache/sis/gui/dataset/package-info.java   |   2 +-
 .../main/java/org/apache/sis/gui/package-info.java |   2 +-
 .../apache/sis/internal/gui/ExceptionReporter.java |  33 +-
 .../org/apache/sis/internal/gui/GUIUtilities.java  |  16 +
 .../gui/PrivateAccess.java}                        |  33 +-
 .../java/org/apache/sis/internal/gui/Styles.java   |   7 +-
 .../org/apache/sis/internal/gui/ToolbarButton.java |   8 +-
 .../internal/gui/control/ColorColumnHandler.java   |   3 +-
 .../sis/internal/gui/control/SyncWindowList.java   | 184 +++++++++++
 .../sis/internal/gui/control/TabularWidget.java    |  80 +++++
 .../sis/internal/gui/control/ValueColorMapper.java |  26 +-
 .../sis/internal/gui/control/package-info.java     |   2 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   3 +-
 24 files changed, 948 insertions(+), 563 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
index 4185e88ba2..9f29c31972 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
@@ -66,7 +66,7 @@ import org.apache.sis.util.resources.Vocabulary;
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -126,7 +126,7 @@ public class DataViewer extends Application {
     /**
      * The window showing system logs. Created when first requested.
      *
-     * @see #showSystemLogsWindow()
+     * @see #showSystemMonitorWindow()
      */
     private Stage systemLogsWindow;
 
@@ -174,10 +174,9 @@ public class DataViewer extends Application {
         final Menu windows = new 
Menu(localized.getString(Resources.Keys.Windows));
         {
             final ObservableList<MenuItem> items = windows.getItems();
-            content.setWindowsItems(items);
-            final MenuItem logging = new 
MenuItem(localized.getString(Resources.Keys.SystemMonitor));
-            logging.setOnAction((e) -> showSystemLogsWindow());
-            items.addAll(content.createNewWindowMenu(), logging);
+            final MenuItem monitor = new 
MenuItem(localized.getString(Resources.Keys.SystemMonitor));
+            monitor.setOnAction((e) -> showSystemMonitorWindow());
+            items.addAll(monitor);
         }
         final Menu help = new Menu(localized.getString(Resources.Keys.Help));
         {   // For keeping variables locale.
@@ -340,7 +339,7 @@ public class DataViewer extends Application {
     /**
      * Shows system logs in a separated window.
      */
-    private void showSystemLogsWindow() {
+    private void showSystemMonitorWindow() {
         if (systemLogsWindow == null) {
             systemLogsWindow = SystemMonitor.create(window, null);
         }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index a40dbed2c1..0fbfcaf717 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -17,6 +17,7 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Locale;
+import java.util.Collections;
 import javafx.scene.control.TitledPane;
 import javafx.scene.layout.GridPane;
 import javafx.scene.layout.Region;
@@ -30,10 +31,12 @@ import javafx.scene.paint.Color;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.gui.dataset.WindowHandler;
 import org.apache.sis.gui.map.MapMenu;
 import org.apache.sis.internal.gui.control.ValueColorMapper;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.control.SyncWindowList;
 import org.apache.sis.util.resources.Vocabulary;
 
 
@@ -68,9 +71,10 @@ final class CoverageControls extends ViewAndControls {
     /**
      * Creates a new set of coverage controls.
      *
-     * @param  owner  the widget which creates this view. Can not be null.
+     * @param  owner   the widget which creates this view. Can not be null.
+     * @param  window  the handler of the window which will show the coverage 
explorer.
      */
-    CoverageControls(final CoverageExplorer owner) {
+    CoverageControls(final CoverageExplorer owner, final WindowHandler window) 
{
         super(owner);
         final Locale     locale     = owner.getLocale();
         final Resources  resources  = Resources.forLocale(locale);
@@ -125,11 +129,17 @@ final class CoverageControls extends ViewAndControls {
         {   // Block for making variables locale to this scope.
             final ValueColorMapper mapper = new ValueColorMapper(resources, 
vocabulary);
             isolines = new IsolineRenderer(view);
-            
isolines.setIsolineTables(java.util.Collections.singletonList(mapper.getSteps()));
+            
isolines.setIsolineTables(Collections.singletonList(mapper.getSteps()));
             final Region view = mapper.getView();
             VBox.setVgrow(view, Priority.ALWAYS);
             isolinesPane = new VBox(view);                          // TODO: 
add band selector
         }
+        /*
+         * Synchronized windows. A synchronized windows is a window which can 
reproduce the same gestures
+         * (zoom, pan, rotation) than the window containing this view. The 
maps displayed in different
+         * windows do not need to use the same map projection; translations 
will be adjusted as needed.
+         */
+        final SyncWindowList windows = new SyncWindowList(window, resources, 
vocabulary);
         /*
          * Put all sections together and have the first one expanded by 
default.
          * The "Properties" section will be built by `PropertyPaneCreator` 
only if requested.
@@ -138,6 +148,7 @@ final class CoverageControls extends ViewAndControls {
         controlPanes = new TitledPane[] {
             new TitledPane(vocabulary.getString(Vocabulary.Keys.Display),  
displayPane),
             new TitledPane(vocabulary.getString(Vocabulary.Keys.Isolines), 
isolinesPane),
+            new TitledPane(resources.getString(Resources.Keys.Windows), 
windows.getView()),
             deferred = new 
TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null)
         };
         /*
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index cbae6baefa..8a86740bb0 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -17,6 +17,7 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.EnumMap;
+import java.util.Optional;
 import java.awt.image.RenderedImage;
 import javafx.application.Platform;
 import javafx.beans.DefaultProperty;
@@ -38,8 +39,11 @@ import org.apache.sis.internal.gui.ToolbarButton;
 import org.apache.sis.internal.gui.NonNullObjectProperty;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
+import org.apache.sis.gui.dataset.WindowHandler;
+import org.apache.sis.gui.dataset.WindowManager;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.Widget;
+import org.apache.sis.internal.gui.PrivateAccess;
 
 
 /**
@@ -207,6 +211,14 @@ public class CoverageExplorer extends Widget {
      */
     private SplitPane content;
 
+    /**
+     * Handler of the window showing this coverage view. This is used for 
creating new windows.
+     * Created when first needed for giving to subclasses a chance to complete 
initialization.
+     *
+     * @see #window()
+     */
+    private WindowHandler window;
+
     /**
      * Creates an initially empty explorer with default view type.
      * By default {@code CoverageExplorer} will show a coverage as a table of 
values,
@@ -216,7 +228,10 @@ public class CoverageExplorer extends Widget {
      * the reason for setting default value to tabular data is because it 
requires loading much less data with
      * {@link java.awt.image.RenderedImage}s supporting deferred tile loading. 
By contrast {@link View#IMAGE}
      * may require loading the full image.</div>
+     *
+     * @deprecated Use {@link #CoverageExplorer(View)}.
      */
+    @Deprecated
     public CoverageExplorer() {
         this(View.TABLE);
     }
@@ -250,10 +265,58 @@ public class CoverageExplorer extends Widget {
      * @param  source  the source explorer from which to take the initial 
coverage or resource.
      *
      * @since 1.2
+     *
+     * @deprecated Replaced by {@code 
source.getImageRequest().ifPresent(newExplorer::setCoverage);}.
      */
+    @Deprecated
     public CoverageExplorer(final CoverageExplorer source) {
         this(source.getViewType());
-        setCoverage(new ImageRequest(source.getResource(), 
source.getCoverage()));
+        source.getImageRequest().ifPresent(this::setCoverage);
+    }
+
+    /*
+     * Hack for giving access outside this package to a field that we do not 
want to make public.
+     * This is a way to simulate the "friend" keyword in C++.
+     */
+    static {
+        PrivateAccess.initWindowHandler = CoverageExplorer::initWindowHandler;
+    }
+
+    /**
+     * Initializes {@link #window} to the given value. This method should be 
invoked soon after
+     * construction and can be invoked only once.
+     */
+    private void initWindowHandler(final WindowHandler handler) {
+        assert Platform.isFxApplicationThread() && window == null : window;
+        window = handler;
+    }
+
+    /**
+     * Returns the handler of the window showing this coverage view. Created 
when first needed
+     * for giving to subclass constructors a chance to complete their 
initialization before the
+     * {@code this} reference is passed to {@link WindowHandler} constructor.
+     */
+    private WindowHandler window() {
+        assert Platform.isFxApplicationThread();
+        if (window == null) {
+            window = WindowHandler.create(this);
+        }
+        return window;
+    }
+
+    /**
+     * Returns a manager of windows showing different view of the coverage.
+     * Those windows are created when the user click on the "New window" 
button.
+     * Each window provides the area where data are shown and where the user 
interacts.
+     * The window can be a JavaFX top-level window ({@link Stage}), but not 
necessarily.
+     * It may also be a tile in a mosaic of windows.
+     *
+     * @return the manager of windows created by the "New window" button.
+     *
+     * @since 1.3
+     */
+    public final WindowManager getWindowManager() {
+        return window().manager;
     }
 
     /**
@@ -280,7 +343,7 @@ public class CoverageExplorer extends Widget {
         if (c == null) {
             switch (type) {
                 case TABLE: c = new GridControls(this); break;
-                case IMAGE: c = new CoverageControls(this); break;
+                case IMAGE: c = new CoverageControls(this, window()); break;
                 default: throw new AssertionError(type);
             }
             views.put(type, c);
@@ -292,11 +355,7 @@ public class CoverageExplorer extends Widget {
          * and became selected (visible).
          */
         if (load) {
-            final GridCoverageResource resource = getResource();
-            final GridCoverage coverage = getCoverage();
-            if (resource != null || coverage != null) {
-                c.load(new ImageRequest(resource, coverage));
-            }
+            getImageRequest().ifPresent(c::load);
         }
         return c;
     }
@@ -321,8 +380,8 @@ public class CoverageExplorer extends Widget {
         if (content == null) {
             /*
              * Prepare buttons to add on the toolbar. Those buttons are not 
managed by this class;
-             * they are managed by org.apache.sis.gui.dataset.DataWindow. We 
only declare here the
-             * text and action for each button.
+             * they are managed by org.apache.sis.gui.dataset.WindowHandler. 
We only declare here
+             * the text and action for each button.
              */
             final ToggleGroup group   = new ToggleGroup();
             final Control[]   buttons = new Control[View.COUNT + 1];
@@ -521,6 +580,7 @@ public class CoverageExplorer extends Widget {
      *
      * @param  source  the coverage or resource to load, or {@code null} if 
none.
      *
+     * @see #getImageRequest()
      * @see GridView#setImage(ImageRequest)
      */
     public final void setCoverage(final ImageRequest source) {
@@ -591,4 +651,25 @@ public class CoverageExplorer extends Widget {
             isCoverageAdjusting = false;
         }
     }
+
+    /**
+     * Returns a request which represent the coverage or resource currently 
shown in this explorer.
+     * This request can be used for showing the same data in another {@code 
CoverageExplorer} instance
+     * by invoking the {@link #setCoverage(ImageRequest)} method.
+     *
+     * @return the request to give to another explorer for showing the same 
coverage.
+     *
+     * @see #setCoverage(ImageRequest)
+     *
+     * @since 1.3
+     */
+    public final Optional<ImageRequest> getImageRequest() {
+        final GridCoverageResource resource = getResource();
+        final GridCoverage coverage = getCoverage();
+        if (resource != null || coverage != null) {
+            return Optional.of(new ImageRequest(resource, coverage));
+        } else {
+            return Optional.empty();
+        }
+    }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
index 06a115d182..392f4b3341 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -47,7 +47,7 @@ import org.opengis.util.InternationalString;
  * that may change in any future version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -199,6 +199,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
          */
         final TableView<Category> table = new TableView<>();
         table.getColumns().add(name);
+        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
         addColumnTo(table, vocabulary.getString(Vocabulary.Keys.Colors));
         /*
          * Add contextual menu items.
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
deleted file mode 100644
index 4f4b120086..0000000000
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/DataWindow.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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 javafx.geometry.Rectangle2D;
-import javafx.stage.Screen;
-import javafx.stage.Stage;
-import javafx.scene.Scene;
-import javafx.scene.Node;
-import javafx.scene.control.Button;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.Tooltip;
-import javafx.scene.control.Labeled;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.Region;
-import javafx.scene.text.Font;
-import org.apache.sis.internal.gui.Resources;
-import org.apache.sis.internal.gui.ToolbarButton;
-
-
-/**
- * Shows features, sample values, map or coverages in a separated window.
- * The data are initially shown in the "Data" pane of {@link ResourceExplorer},
- * but may be copied in a separated, usually bigger, windows.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   1.1
- * @module
- */
-final class DataWindow extends Stage {
-    /**
-     * The tools bar. Removed from the pane when going in full screen mode, 
and reinserted
-     * when exiting full screen mode.
-     *
-     * @see #onFullScreen(boolean)
-     */
-    private final ToolBar tools;
-
-    /**
-     * Creates a new window for the given data selected in the explorer or 
determined by the active tab.
-     * The new window will be positioned in the screen center but not yet 
shown.
-     *
-     * @param  home   the window containing the main explorer, to be the 
target of "home" button.
-     * @param  data   the data selected by user, to show in a new window.
-     */
-    DataWindow(final Stage home, final SelectedData data) {
-        final Region content = data.createView();
-        /*
-         * Build the tools bar. This bar will be hidden in full screen mode. 
Note that above
-         * method assumes that the "home" button created below is the first 
one in the toolbar.
-         */
-        final Button mainWindow = new Button("\u2302\uFE0F");               // 
⌂ — house
-        mainWindow.setTooltip(new 
Tooltip(data.localized.getString(Resources.Keys.MainWindow)));
-        mainWindow.setOnAction((e) -> {home.show(); home.toFront();});
-
-        final Button fullScreen = new Button("\u21F1\uFE0F");               // 
⇱ — North West Arrow to Corner
-        fullScreen.setTooltip(new 
Tooltip(data.localized.getString(Resources.Keys.FullScreen)));
-        fullScreen.setOnAction((e) -> setFullScreen(true));
-        fullScreenProperty().addListener((source, oldValue, newValue) -> 
onFullScreen(newValue));
-
-        tools = new ToolBar(mainWindow, fullScreen);
-        /*
-         * Add content-specific buttons. We use the 
"org.apache.sis.gui.ToolbarButton" property
-         * as a way to transfer ToolbarButton accross packages without making 
this class public.
-         */
-        tools.getItems().addAll(ToolbarButton.remove(content));
-        /*
-         * After we finished adding all buttons, set the font of all of them 
to a larger size.
-         */
-        final Font font = Font.font(20);
-        for (final Node node : tools.getItems()) {
-            if (node instanceof Labeled) {
-                ((Labeled) node).setFont(font);
-            }
-        }
-        /*
-         * Main content. After this constructor returned, caller
-         * should set the width and height, then show the window.
-         */
-        final BorderPane pane = new BorderPane();
-        pane.setTop(tools);
-        pane.setCenter(content);
-        setScene(new Scene(pane));
-        /*
-         * We use an initial size covering a large fraction of the screen 
because
-         * this window is typically used for showing image or large tabular 
data.
-         */
-        final Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
-        setWidth (0.8 * bounds.getWidth());
-        setHeight(0.8 * bounds.getHeight());
-    }
-
-    /**
-     * Invoked when entering or existing the full screen mode.
-     * Used for hiding/showing the toolbar when entering/exiting full screen 
mode.
-     */
-    private void onFullScreen(final boolean entering) {
-        final BorderPane pane = (BorderPane) getScene().getRoot();
-        pane.setTop(entering ? null : tools);
-    }
-}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index 3f09fd1fb9..73881dd80b 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -19,18 +19,18 @@ package org.apache.sis.gui.dataset;
 import java.util.EnumMap;
 import java.util.Objects;
 import java.util.Collection;
+import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.property.ReadOnlyProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
-import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
+import javafx.collections.ListChangeListener;
 import javafx.event.EventHandler;
 import javafx.concurrent.Task;
 import javafx.scene.layout.Region;
 import javafx.scene.control.Accordion;
-import javafx.scene.control.ContextMenu;
 import javafx.scene.control.SplitPane;
 import javafx.scene.control.Tab;
 import javafx.scene.control.TabPane;
@@ -44,6 +44,7 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.gui.Widget;
 import org.apache.sis.gui.metadata.MetadataSummary;
 import org.apache.sis.gui.metadata.MetadataTree;
 import org.apache.sis.gui.metadata.StandardMetadataTree;
@@ -51,7 +52,6 @@ import org.apache.sis.gui.coverage.ImageRequest;
 import org.apache.sis.gui.coverage.CoverageExplorer;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.LogHandler;
@@ -59,14 +59,16 @@ import org.apache.sis.internal.gui.LogHandler;
 
 /**
  * A panel showing a {@linkplain ResourceTree tree of resources} together with 
their metadata and data views.
+ * This panel contains also a "new window" button for creating new windows 
showing the same data but potentially
+ * a different locations and times. {@code ResourceExplorer} contains a list 
of windows created by this widget.
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
-public class ResourceExplorer extends WindowManager {
+public class ResourceExplorer extends Widget {
     /**
      * The tree of resources.
      */
@@ -169,69 +171,54 @@ public class ResourceExplorer extends WindowManager {
      */
     public ResourceExplorer() {
         /*
-         * Build the resource explorer. Must be first because `localized()` 
depends on it.
-         * Then build the controls on the left side, which will initially 
contain only the
-         * resource explorer. The various tabs will be next (on the right 
side).
+         * Build the controls on the left side, which will initially contain 
only the resource explorer.
+         * The various tabs will be next (on the right side).
          */
-        resources = new ResourceTree();
+        resources  = new ResourceTree();
         
resources.getSelectionModel().getSelectedItems().addListener(this::onResourceSelected);
         resources.setPrefWidth(400);
-        selectedResource = new ReadOnlyObjectWrapper<>(this, 
"selectedResource");
         final Vocabulary vocabulary = 
Vocabulary.getResources(resources.locale);
         final TitledPane resourcesPane = new 
TitledPane(vocabulary.getString(Vocabulary.Keys.Resources), resources);
         controls = new Accordion(resourcesPane);
         controls.setExpandedPane(resourcesPane);
-        SplitPane.setResizableWithParent(controls, Boolean.FALSE);
         expandedPane = new EnumMap<>(CoverageExplorer.View.class);
         /*
-         * "Summary" tab showing a summary of resource metadata.
+         * Prepare content of tab panes.
+         * "Native metadata" tab will show metadata in their "raw" form 
(specific to the format).
+         * "Logging" tab will show log records specific to the selected 
resource
+         * (as opposed to the application menu showing all loggings regardless 
their source).
          */
         metadata = new MetadataSummary();
-        final Tab summaryTab = new 
Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView());
-        /*
-         * "Visual" tab showing the raster data as an image.
-         */
-        viewTab = new Tab(vocabulary.getString(Vocabulary.Keys.Visual));
-        viewTab.setContextMenu(new ContextMenu(createNewWindowMenu()));
-        /*
-         * "Data" tab showing raster data as a table.
-         */
-        tableTab = new Tab(vocabulary.getString(Vocabulary.Keys.Data));
-        tableTab.setContextMenu(new ContextMenu(createNewWindowMenu()));
-        /*
-         * "Metadata" tab showing ISO 19115 metadata as a tree.
-         */
-        final Tab metadataTab = new 
Tab(vocabulary.getString(Vocabulary.Keys.Metadata), new 
StandardMetadataTree(metadata));
-        /*
-         * "Native metadata" tab showing metadata in their "raw" form 
(specific to the format).
-         */
         nativeMetadata = new MetadataTree(metadata);
-        defaultNativeTabLabel = vocabulary.getString(Vocabulary.Keys.Format);
-        nativeMetadataTab = new Tab(defaultNativeTabLabel, nativeMetadata);
-        nativeMetadataTab.setDisable(true);
-        /*
-         * "Logging" tab showing log records specific to the selected resource
-         * (as opposed to the application menu showing all loggings regardless 
their source).
-         */
         final LogViewer logging = new LogViewer(vocabulary);
+        selectedResource = new ReadOnlyObjectWrapper<>(this, 
"selectedResource");
         logging.source.bind(selectedResource);
-        final Tab loggingTab = new 
Tab(vocabulary.getString(Vocabulary.Keys.Logs), logging.getView());
-        loggingTab.disableProperty().bind(logging.isEmptyProperty());
+        final Tab summaryTab, metadataTab, loggingTab;
+        final TabPane tabs = new TabPane(
+            summaryTab        = new 
Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView()),
+            viewTab           = new 
Tab(vocabulary.getString(Vocabulary.Keys.Visual)),
+            tableTab          = new 
Tab(vocabulary.getString(Vocabulary.Keys.Data)),
+            metadataTab       = new 
Tab(vocabulary.getString(Vocabulary.Keys.Metadata), new 
StandardMetadataTree(metadata)),
+            nativeMetadataTab = new 
Tab(vocabulary.getString(Vocabulary.Keys.Format),   nativeMetadata),
+            loggingTab        = new 
Tab(vocabulary.getString(Vocabulary.Keys.Logs),     logging.getView()));
+
+        tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+        tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        defaultNativeTabLabel = nativeMetadataTab.getText();
+        nativeMetadataTab.setDisable(true);
         /*
          * Build the main pane which put everything together.
          */
-        final TabPane tabs = new TabPane(summaryTab, viewTab, tableTab, 
metadataTab, nativeMetadataTab, loggingTab);
-        tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
-        tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
         content = new SplitPane(controls, tabs);
         content.setDividerPosition(0, 1./3);
-        SplitPane.setResizableWithParent(resources, Boolean.FALSE);
-        SplitPane.setResizableWithParent(tabs, Boolean.TRUE);
+        SplitPane.setResizableWithParent(controls, Boolean.FALSE);
+        SplitPane.setResizableWithParent(tabs,     Boolean.TRUE);
         /*
          * Register listeners last, for making sure we do not have undesired 
events.
          * Those listeners trig loading of various objects (data, standard 
metadata,
          * native metadata) when the corresponding tab become visible.
          */
+        loggingTab.disableProperty().bind(logging.isEmptyProperty());
         dataShown = viewTab.selectedProperty().or(tableTab.selectedProperty());
         dataShown.addListener((p,o,n) -> {
             if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) {
@@ -254,11 +241,11 @@ public class ResourceExplorer extends WindowManager {
     }
 
     /**
-     * Returns resources for current locale.
+     * Returns the locale for controls and messages.
      */
     @Override
-    final Resources localized() {
-        return resources.localized();
+    public final Locale getLocale() {
+        return resources.locale;
     }
 
     /**
@@ -278,6 +265,8 @@ public class ResourceExplorer extends WindowManager {
      * This is an accessor for the {@link ResourceTree#onResourceLoaded} 
property value.
      *
      * @return current function to be called after a resource has been loaded, 
or {@code null} if none.
+     *
+     * @see ResourceTree#onResourceLoaded
      */
     public EventHandler<ResourceEvent> getOnResourceLoaded() {
         return resources.onResourceLoaded.get();
@@ -289,6 +278,8 @@ public class ResourceExplorer extends WindowManager {
      * If this method is never invoked, then the default value is {@code null}.
      *
      * @param  handler  new function to be called after a resource has been 
loaded, or {@code null} if none.
+     *
+     * @see ResourceTree#onResourceLoaded
      */
     public void setOnResourceLoaded(final EventHandler<ResourceEvent> handler) 
{
         resources.onResourceLoaded.set(handler);
@@ -300,6 +291,8 @@ public class ResourceExplorer extends WindowManager {
      *
      * @return current function to be called when a resource is closed, or 
{@code null} if none.
      *
+     * @see ResourceTree#onResourceClosed
+     *
      * @since 1.2
      */
     public EventHandler<ResourceEvent> getOnResourceClosed() {
@@ -313,6 +306,8 @@ public class ResourceExplorer extends WindowManager {
      *
      * @param  handler  new function to be called when a resource is closed, 
or {@code null} if none.
      *
+     * @see ResourceTree#onResourceClosed
+     *
      * @since 1.2
      */
     public void setOnResourceClosed(final EventHandler<ResourceEvent> handler) 
{
@@ -350,6 +345,24 @@ public class ResourceExplorer extends WindowManager {
         resources.removeAndClose(resource);
     }
 
+    /**
+     * Returns the currently selected resource.
+     *
+     * @return the currently selected resource, or {@code null} if none.
+     */
+    public final Resource getSelectedResource() {
+        return selectedResource.get();
+    }
+
+    /**
+     * Returns the property for currently selected resource.
+     *
+     * @return property for currently selected resource.
+     */
+    public final ReadOnlyProperty<Resource> selectedResourceProperty() {
+        return selectedResource.getReadOnlyProperty();
+    }
+
     /**
      * Invoked in JavaFX thread when a new item is selected in the resource 
tree.
      * Normally, only one resource is selected since we use a single selection 
model.
@@ -490,7 +503,6 @@ public class ResourceExplorer extends WindowManager {
         if (image    != null) viewTab .setContent(image);
         if (table    != null) tableTab.setContent(table);
         final boolean isEmpty = (image == null & table == null);
-        setNewWindowDisabled(isEmpty);
         /*
          * Add or remove controls for the selected view.
          * Information about the expanded pane needs to be saved before to 
remove controls,
@@ -516,67 +528,6 @@ public class ResourceExplorer extends WindowManager {
         return !isEmpty | (resource == null);
     }
 
-    /**
-     * Returns the set of currently selected data, or {@code null} if none.
-     * This is invoked when the user selects the "New window" menu item.
-     */
-    @Override
-    final SelectedData getSelectedData() {
-        final Resource resource = getSelectedResource();
-        if (resource == null) {
-            return null;
-        }
-        FeatureTable     table = null;
-        CoverageExplorer grid  = null;
-        if (resource instanceof GridCoverageResource) {
-            /*
-             * Want the full coverage in all bands (sample dimensions).
-             */
-            if (coverage == null) {
-                updateDataTab(resource);                // For forcing 
creation of CoverageExplorer.
-            }
-            grid = coverage;
-        } else if (resource instanceof FeatureSet) {
-            /*
-             * We will not set features in an initially empty `FeatureTable` 
(to be newly created),
-             * but instead share the `FeatureLoader` created by the feature 
table of this explorer.
-             * We do that even if the feature table is not currently visible. 
This will not cause
-             * useless data loading since they share the same `FeatureLoader`.
-             */
-            if (features == null) {
-                updateDataTab(resource);                // For forcing 
creation of FeatureTable.
-            }
-            table = features;
-        } else {
-            return null;
-        }
-        String text;
-        try {
-            text = ResourceTree.findLabel(resource, resources.locale, true);
-        } catch (DataStoreException | RuntimeException e) {
-            text = 
Vocabulary.getResources(resources.locale).getString(Vocabulary.Keys.Unnamed);
-        }
-        return new SelectedData(text, table, grid, localized());
-    }
-
-    /**
-     * Returns the currently selected resource.
-     *
-     * @return the currently selected resource, or {@code null} if none.
-     */
-    public final Resource getSelectedResource() {
-        return selectedResource.get();
-    }
-
-    /**
-     * Returns the property for currently selected resource.
-     *
-     * @return property for currently selected resource.
-     */
-    public final ReadOnlyProperty<Resource> selectedResourceProperty() {
-        return selectedResource.getReadOnlyProperty();
-    }
-
     /**
      * If the given resource is not one of the resource that {@link 
#updateDataTab(Resource)} can handle,
      * searches in a background thread for a default resource to show. The 
purpose of this method is to
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
deleted file mode 100644
index 184a89c184..0000000000
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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 javafx.scene.layout.Region;
-import org.apache.sis.gui.coverage.CoverageExplorer;
-import org.apache.sis.internal.gui.Resources;
-
-
-/**
- * A description of currently selected data.
- * The selected data may be one of the following resources:
- *
- * <ul>
- *   <li>{@link org.apache.sis.storage.FeatureSet}</li>
- *   <li>{@link org.apache.sis.storage.GridCoverageResource}</li>
- * </ul>
- *
- * {@code SelectedData} does not contain those resources directly, but rather 
contains the view or
- * other kind of object wrapping the selected resource. The kind of wrappers 
used for each type of
- * resource may change in any future version of this class.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   1.1
- * @module
- */
-final class SelectedData {
-    /**
-     * A title to use for windows and menu items.
-     */
-    final String title;
-
-    /**
-     * The control that contains the currently selected data if those data are 
features.
-     * Only one of {@link #features} and {@link #coverage} shall be non-null.
-     */
-    private final FeatureTable features;
-
-    /**
-     * The request for coverage data, or {@code null} if the selected data are 
not coverage.
-     * Only one of {@link #features} and {@link #coverage} shall be non-null.
-     */
-    private final CoverageExplorer coverage;
-
-    /**
-     * Localized resources, for convenience only.
-     */
-    final Resources localized;
-
-    /**
-     * Creates a snapshot of selected data.
-     * Only one of {@code features} and {@code coverage} shall be non-null.
-     *
-     * @param  title      a title to use for windows and menu items.
-     * @param  features   control that contains the currently selected data if 
those data are features.
-     * @param  coverage   the request for coverage data.
-     * @param  localized  localized resources, for convenience only.
-     */
-    SelectedData(final String title, final FeatureTable features, final 
CoverageExplorer coverage, final Resources localized) {
-        this.title     = title;
-        this.features  = features;
-        this.coverage  = coverage;
-        this.localized = localized;
-    }
-
-    /**
-     * Creates the view for selected data.
-     */
-    final Region createView() {
-        if (features != null) {
-            return new FeatureTable(features);
-        } else {
-            return new CoverageExplorer(coverage).getView();
-        }
-    }
-}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
new file mode 100644
index 0000000000..d324cdb311
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
@@ -0,0 +1,352 @@
+/*
+ * 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 java.util.logging.Logger;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import javafx.scene.layout.Region;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.property.StringProperty;
+import javafx.beans.property.SimpleStringProperty;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.gui.coverage.CoverageExplorer;
+import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.PrivateAccess;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * A separated window for visualizing a resource managed by {@link 
ResourceExplorer}.
+ * A window provides the area where the data are shown and where the user 
interacts.
+ * The window can be a JavaFX top-level window ({@link Stage}), but not 
necessarily.
+ * It may also be a tile in a mosaic of windows.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public abstract class WindowHandler {
+    /**
+     * The window manager which contains this handler.
+     * The manager contains the list of all windows created for the same 
widget.
+     */
+    public final WindowManager manager;
+
+    /**
+     * The window where the resource is visualized. This is created when first 
needed.
+     * We assume that resource views do not change their window during their 
lifetime.
+     *
+     * @see #show()
+     */
+    private Stage window;
+
+    /**
+     * The property for a label that identify the view. If the resource is 
shown
+     * in a top-level window, then this is typically the title of that window.
+     */
+    public final StringProperty title;
+
+    /**
+     * The listener to add and remove to/from the {@link #title} property. We 
use a static reference for avoiding
+     * to retain a direct reference to {@link #window} in listeners, which 
would increase the risk of memory leak.
+     */
+    private static final ChangeListener<String> TITLE_CHANGED = (p,o,n) -> {
+        final WindowHandler handler = (WindowHandler) ((StringProperty) 
p).getBean();
+        handler.window.setTitle(n + " — Apache SIS");
+    };
+
+    /**
+     * Creates a new handler for a window showing a resource.
+     * Exactly one of {@code creator} or {@code locale} arguments should be 
non-null.
+     *
+     * @param creator  the handler which is duplicated, or {@code null} if 
none.
+     * @param locale   language of texts. Used only if {@code creator} is null.
+     */
+    WindowHandler(final WindowHandler creator, final Locale locale) {
+        manager = (creator != null) ? creator.manager : new 
WindowManager(this, locale);
+        title = new SimpleStringProperty(this, "title");
+    }
+
+    /**
+     * Methods to be invoked last by constructors, after everything else 
succeeded.
+     * Construction must be completed before to invoke this method because 
this call will notify listeners.
+     *
+     * @return {@code this} for method call chaining.
+     */
+    final WindowHandler finish() {
+        String text;
+        if (manager.main == this) {
+            text = 
Resources.forLocale(manager.locale).getString(Resources.Keys.MainWindow);
+        } else try {
+            text = ResourceTree.findLabel(getResource(), manager.locale, true);
+        } catch (DataStoreException | RuntimeException e) {
+            text = 
Vocabulary.getResources(manager.locale).getString(Vocabulary.Keys.Unknown);
+            
Logging.recoverableException(Logger.getLogger(Modules.APPLICATION), 
WindowHandler.class, "<init>", e);
+        }
+        title.set(text);
+        manager.modifiableWindowList.add(this);
+        return this;
+    }
+
+    /**
+     * Creates a new handler for the window which is showing the given 
coverage viewer.
+     *
+     * @param  widget the widget for which to create a handler.
+     * @return a handler for the window of the given widget.
+     */
+    public static WindowHandler create(final CoverageExplorer widget) {
+        ArgumentChecks.ensureNonNull("widget", widget);
+        return new ForCoverage(null, widget.getLocale(), widget).finish();
+    }
+
+    /**
+     * Creates a new handler for the window which is showing the given table 
of features.
+     *
+     * @param  widget the widget for which to create a handler.
+     * @return a handler for the window of the given widget.
+     */
+    public static WindowHandler create(final FeatureTable widget) {
+        ArgumentChecks.ensureNonNull("widget", widget);
+        return new ForFeatures(null, widget.textLocale, widget).finish();
+    }
+
+    /**
+     * Prepares a new window with the same content than the window managed by 
this handler.
+     * This method can be used for creating many windows over the same data.
+     * Each window can do pans, zooms and rotations independently of other 
windows,
+     * or be synchronized with other windows, at user's choice.
+     *
+     * <p>The new view is added to the {@link WindowManager#windows} list and 
will be removed
+     * from that list if the window is closed. If the resource is closed in 
the window manager,
+     * then all windows showing that resource will be closed.</p>
+     *
+     * <p>The new window is not initially visible.
+     * To show the window, invoke {@link #show()} on the returned handler.</p>
+     *
+     * @return information about the new window.
+     */
+    public abstract WindowHandler duplicate();
+
+    /**
+     * Returns the JavaFX region where the resource is shown. This value shall 
be stable.
+     */
+    abstract Region getView();
+
+    /**
+     * Returns the window which is showing the resource.
+     * This is used for fetching the main window.
+     */
+    Window getWindow() {
+        return GUIUtilities.getWindow(getView());
+    }
+
+    /**
+     * Shows the window and brings it to the front.
+     * For handlers created by a {@code create(…)} method, this {@code show()} 
method can be invoked at any time.
+     * For handlers created by {@link #duplicate()}, this {@code show()} 
method can be invoked as long as the window
+     * has not been closed. After a duplicated window has been closed, it is 
not possible to show it again.
+     *
+     * @throws IllegalStateException if this handler is a {@linkplain 
#duplicate() duplicate}
+     *         and the window has been closed.
+     */
+    public void show() {
+        if (window == null) {
+            if (manager.main == this) {
+                Window w = getWindow();
+                if (w instanceof Stage) {
+                    window = (Stage) w;
+                } else {
+                    return;
+                }
+            } else {
+                if (getResource() == null) {
+                    throw new 
IllegalStateException(Errors.format(Errors.Keys.DisposedInstanceOf_1, 
getClass()));
+                }
+                window = manager.newWindow(getView());
+                window.setOnHidden((e) -> dispose());
+                title.addListener(TITLE_CHANGED);
+                TITLE_CHANGED.changed(title, null, title.get());
+            }
+        }
+        window.show();
+        window.toFront();
+    }
+
+    /**
+     * The resource shown in the {@linkplain #window}, or {@code null} if 
unspecified.
+     * This is used for identifying which handlers to remove when a resource 
is closed.
+     * This method is not yet public because a future version may need to 
return a full
+     * map context instead of a single resource.
+     */
+    abstract Resource getResource();
+
+    /**
+     * Invoked when the window is hidden. After removing this handler from the 
windows list,
+     * this method makes a "best effort" for helping the garbage-collector to 
release memory.
+     */
+    void dispose() {
+        assert manager.main != this;                // Because listener is not 
registered for main window.
+        manager.modifiableWindowList.remove(this);
+        title.removeListener(TITLE_CHANGED);
+        if (window != null) {
+            window.setScene(null);
+            window = null;
+        }
+    }
+
+
+
+    /**
+     * A visualization managed by a {@link CoverageExplorer} instance.
+     * The initial {@code CoverageExplorer} instance (before duplication)
+     * is itself produced by a {@link ResourceExplorer}.
+     */
+    private static final class ForCoverage extends WindowHandler {
+        /**
+         * The widget providing the view.
+         */
+        private final CoverageExplorer widget;
+
+        /**
+         * Creates a new handler for a window showing a resource.
+         *
+         * @param creator  the handler which is duplicated, or {@code null} if 
none.
+         * @param locale   language of texts. Used only if {@code creator} is 
null.
+         * @param widget   the widget providing the view of the resource.
+         */
+        ForCoverage(final WindowHandler creator, final Locale locale, final 
CoverageExplorer widget) {
+            super(creator, locale);
+            this.widget = widget;
+        }
+
+        /**
+         * Returns the JavaFX region where the resource is shown.
+         */
+        @Override
+        Region getView() {
+            return widget.getView();
+        }
+
+        /**
+         * Returns the window which is showing the resource. We avoid the call 
to {@link #getView()}
+         * because in the particular case of {@link CoverageExplorer}, it 
causes the initialization
+         * of a splitted pane which is not the one used by the main window.
+         */
+        @Override
+        Window getWindow() {
+            return 
GUIUtilities.getWindow(widget.getDataView(widget.getViewType()));
+        }
+
+        /**
+         * Prepares (without showing) a new window with the same content than 
the window managed by this handler.
+         */
+        @Override
+        public WindowHandler duplicate() {
+            final CoverageExplorer explorer = new 
CoverageExplorer(widget.getViewType());
+            final ForCoverage handler = new ForCoverage(this, null, explorer);
+            PrivateAccess.initWindowHandler.accept(explorer, handler);
+            widget.getImageRequest().ifPresent(explorer::setCoverage);
+            return handler.finish();
+        }
+
+        /**
+         * The resource shown in the {@linkplain #window window}, or {@code 
null} if unspecified.
+         */
+        @Override
+        Resource getResource() {
+            return widget.getResource();
+        }
+
+        /**
+         * Makes a "best effort" for helping the garbage-collector to release 
resources.
+         */
+        @Override
+        void dispose() {
+            super.dispose();
+            widget.setResource(null);
+        }
+    }
+
+
+
+
+    /**
+     * A visualization managed by a {@link FeatureTable} instance.
+     * The initial {@code FeatureTable} instance (before duplication)
+     * is itself produced by a {@link ResourceExplorer}.
+     */
+    private static final class ForFeatures extends WindowHandler {
+        /**
+         * The widget providing the view.
+         */
+        private final FeatureTable widget;
+
+        /**
+         * Creates a new handler for a window showing a resource.
+         *
+         * @param creator  the handler which is duplicated, or {@code null} if 
none.
+         * @param locale   language of texts. Used only if {@code creator} is 
null.
+         * @param widget   the widget providing the view of the resource.
+         */
+        ForFeatures(final WindowHandler creator, final Locale locale, final 
FeatureTable widget) {
+            super(creator, locale);
+            this.widget = widget;
+        }
+
+        /**
+         * Returns the JavaFX region where the resource is shown.
+         */
+        @Override
+        Region getView() {
+            return widget;
+        }
+
+        /**
+         * Prepares (without showing) a new window with the same content than 
the window managed by this handler.
+         */
+        @Override
+        public WindowHandler duplicate() {
+            return new ForFeatures(this, null, new 
FeatureTable(widget)).finish();
+        }
+
+        /**
+         * The resource shown in the {@linkplain #window window}, or {@code 
null} if unspecified.
+         */
+        @Override
+        Resource getResource() {
+            return widget.getFeatures();
+        }
+
+        /**
+         * Makes a "best effort" for helping the garbage-collector to release 
resources.
+         */
+        @Override
+        void dispose() {
+            super.dispose();
+            widget.setFeatures(null);
+        }
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
index f00a78a519..002bbcdfe2 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java
@@ -16,198 +16,122 @@
  */
 package org.apache.sis.gui.dataset;
 
-import java.util.List;
-import java.util.ArrayList;
 import java.util.Locale;
-import javafx.collections.ObservableList;
 import javafx.stage.Stage;
-import javafx.scene.control.MenuItem;
-import javafx.scene.control.SeparatorMenuItem;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanPropertyBase;
+import javafx.stage.Screen;
+import javafx.scene.Scene;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Labeled;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Font;
+import javafx.geometry.Rectangle2D;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
 import org.apache.sis.internal.gui.Resources;
-import org.apache.sis.gui.Widget;
+import org.apache.sis.internal.gui.ToolbarButton;
 
 
 /**
- * Manages the list of opened {@link DataWindow}s.
+ * A list of windows showing resources managed by {@link ResourceExplorer}.
+ * Windows are created when user clicks on the "New window" button.
+ * Many windows can be created for the same resource.
+ * Each window can apply different styles or map projections.
+ * Gestures such as zooms, pans and rotations can be applied independently
+ * or synchronized between windows, at user's choice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   1.1
+ * @version 1.3
+ * @since   1.3
  * @module
  */
-abstract class WindowManager extends Widget {
-    /**
-     * The contextual menu items for creating a new window showing selected 
data.
-     * All menus in this list will be enabled or disabled depending on whether 
there is data to show.
-     */
-    private final List<MenuItem> newWindowMenus;
-
+public final class WindowManager {          // Not designed for subclassing.
     /**
-     * The menu items for navigating to different windows. {@link 
WindowManager} will automatically
-     * add or remove elements in that list when new windows are created or 
closed.
-     *
-     * @see #setWindowsItems(ObservableList)
+     * The handler of the main window. This handler shall never be disposed.
      */
-    private ObservableList<MenuItem> showWindowMenus;
-
-    /**
-     * A property telling whether at least one data window created by this 
class is still visible.
-     *
-     * @see #createNewWindowMenu()
-     * @see #setWindowsItems(ObservableList)
-     */
-    public final ReadOnlyBooleanProperty hasWindowsProperty;
-
-    /**
-     * The {@link WindowManager#hasWindowsProperty} property implementation.
-     */
-    private final class WindowsProperty extends ReadOnlyBooleanPropertyBase {
-        /** The property value. */
-        private boolean hasWindows;
-
-        /** Sets this property to the given value. */
-        final void set(final boolean value) {
-            hasWindows = value;
-            fireValueChangedEvent();
-        }
-
-        /** Returns the current property value. */
-        @Override public boolean get()    {return hasWindows;}
-        @Override public Object getBean() {return WindowManager.this;}
-        @Override public String getName() {return "hasWindows";}
-    }
-
-    /**
-     * Creates a new manager of windows.
-     */
-    WindowManager() {
-        newWindowMenus     = new ArrayList<>(3);
-        hasWindowsProperty = new WindowsProperty();
-    }
+    final WindowHandler main;
 
     /**
-     * Returns resources for current locale. We could fetch this information 
ourselves,
-     * but we currently ask to subclass because it has this information anyway.
-     *
-     * @return the resources in current locale.
+     * The language of texts to show to the user.
      */
-    abstract Resources localized();
+    final Locale locale;
 
     /**
-     * Returns the locale for controls and messages.
+     * Read-only list of windows showing resources managed by {@link 
ResourceExplorer}.
+     * Items are added in this list when the user clicks on "New window" 
button and are
+     * removed from this list when the user closes the window.
      */
-    @Override
-    public final Locale getLocale() {
-        return localized().getLocale();
-    }
+    public final ObservableList<WindowHandler> windows;
 
     /**
-     * Creates a menu item for creating new windows for the currently selected 
resource.
-     * The new menu item is initially disabled. Its will become enabled 
automatically when
-     * a resource is selected.
-     *
-     * <p>Note: current implementation keeps a strong reference to created 
menu.
-     * Use this method only for menus that are expected to exist for 
application lifetime.</p>
-     *
-     * @return a "new window" menu item.
-     *
-     * @see #hasWindowsProperty
+     * Modifiable list where to append new windows when they are created.
+     * This is the source of {@link #windows} list.
      */
-    public final MenuItem createNewWindowMenu() {
-        final MenuItem menu = localized().menu(Resources.Keys.NewWindow, (e) 
-> newDataWindow());
-        menu.setDisable(true);
-        newWindowMenus.add(menu);
-        return menu;
-    }
+    final ObservableList<WindowHandler> modifiableWindowList;
 
     /**
-     * Sets the list where to add or remove the name of data windows. New data 
windows are created when user
-     * selects a menu item given by {@link #createNewWindowMenu()}. {@code 
WindowManager} will automatically
-     * add or remove elements in the given list. The position of the new menu 
item will be just before the
-     * last {@link SeparatorMenuItem} instance. If no {@code 
SeparatorMenuItem} is found, then one will be
-     * inserted at the beginning of the given list when needed.
+     * Creates a new window manager.
      *
-     * @param  items  the list where to add and remove the name of windows.
-     *
-     * @see #hasWindowsProperty
-     * @see #createNewWindowMenu()
+     * @param  main    handler of the main window. This handler shall never be 
disposed.
+     * @param  locale  the language of texts to show to the user.
      */
-    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
-    public void setWindowsItems(final ObservableList<MenuItem> items) {
-        showWindowMenus = items;
+    WindowManager(final WindowHandler main, final Locale locale) {
+        this.main   = main;
+        this.locale = locale;
+        modifiableWindowList = FXCollections.observableArrayList();
+        windows = 
FXCollections.unmodifiableObservableList(modifiableWindowList);
     }
 
     /**
-     * Enables or disables all "new window" menus. Those menu should be 
disabled when the current selection
-     * does not contain any data we can show.
-     */
-    final void setNewWindowDisabled(final boolean disabled) {
-        for (final MenuItem m : newWindowMenus) {
-            m.setDisable(disabled);
-        }
-    }
-
-    /**
-     * Returns the set of currently selected data, or {@code null} if none.
+     * Creates a new window for the specified widget together with its toolbar.
+     * The new window will be positioned in the screen center but not yet 
shown.
+     * The window will have initially no title (title should be set by caller).
      *
-     * @return the selected data, or {@code null} if none.
-     */
-    abstract SelectedData getSelectedData();
-
-    /**
-     * Invoked when user asked to show the data in a new window. This method 
may be invoked from various sources:
-     * contextual menu on the tab, contextual menu in the explorer tree, or 
from the "new window" menu item.
-     */
-    private void newDataWindow() {
-        final SelectedData selection = getSelectedData();
-        if (selection != null) {
-            final DataWindow window = new DataWindow((Stage) 
getView().getScene().getWindow(), selection);
-            window.setTitle(selection.title + " — Apache SIS");
-            window.show();
-            if (showWindowMenus != null) {
-                /*
-                 * Search for insertion point just before the menu separator.
-                 * If no menu separator is found, add one.
-                 */
-                int insertAt = showWindowMenus.size();
-                do if (--insertAt < 0) {
-                    showWindowMenus.add(insertAt = 0, new SeparatorMenuItem());
-                    ((WindowsProperty) hasWindowsProperty).set(true);
-                    break;
-                } while (!(showWindowMenus.get(insertAt) instanceof 
SeparatorMenuItem));
-                final MenuItem menu = new MenuItem(selection.title);
-                menu.setOnAction((e) -> window.toFront());
-                showWindowMenus.add(insertAt, menu);
-                window.setOnHidden((e) -> removeDataWindow(menu));
-            }
-        }
-    }
-
-    /**
-     * Invoked when a window has been hidden. This method removes the window 
title from the "Windows" menu.
-     * The hidden window will be garbage collected at some later time.
+     * @param  content  control that contains the data to show in a new window.
+     * @return window for showing the resource. Untitled and not yet visible.
      */
-    private void removeDataWindow(final MenuItem menu) {
-        final ObservableList<MenuItem> items = showWindowMenus;
-        if (items != null) {
-            for (int i = items.size(); --i >= 0;) {
-                if (items.get(i) == menu) {
-                    items.remove(i);
-                    if (i == 0) {
-                        if (!items.isEmpty()) {
-                            if (items.get(0) instanceof SeparatorMenuItem) {
-                                items.remove(0);
-                            } else {
-                                break;      // Some other windows are still 
present.
-                            }
-                        }
-                        ((WindowsProperty) hasWindowsProperty).set(false);
-                    }
-                    break;
-                }
+    final Stage newWindow(final Region content) {
+        final Stage     stage      = new Stage();
+        final Button    mainWindow = new Button("\u2302\uFE0F");        // ⌂ — 
house
+        final Button    fullScreen = new Button("\u21F1\uFE0F");        // ⇱ — 
North West Arrow to Corner
+        final Resources localized  = Resources.forLocale(locale);
+        mainWindow.setTooltip(new 
Tooltip(localized.getString(Resources.Keys.MainWindow)));
+        fullScreen.setTooltip(new 
Tooltip(localized.getString(Resources.Keys.FullScreen)));
+        mainWindow.setOnAction((e) -> main.show());
+        fullScreen.setOnAction((e) -> stage.setFullScreen(true));
+        /*
+         * Add content-specific buttons. We use the 
"org.apache.sis.gui.ToolbarButton" property
+         * as a way to transfer ToolbarButton accross packages without making 
this class public.
+         */
+        final ToolBar tools = new ToolBar(mainWindow, fullScreen);
+        tools.getItems().addAll(ToolbarButton.remove(content));
+        /*
+         * After we finished adding all buttons, set the font of all of them 
to a larger size.
+         */
+        final Font font = Font.font(20);
+        for (final Node node : tools.getItems()) {
+            if (node instanceof Labeled) {
+                ((Labeled) node).setFont(font);
             }
         }
+        /*
+         * Main content. The tools bar will be hidden in full screen mode.
+         */
+        final BorderPane pane = new BorderPane();
+        stage.fullScreenProperty().addListener((p,o,entering) -> 
pane.setTop(entering ? null : tools));
+        pane.setTop(tools);
+        pane.setCenter(content);
+        stage.setScene(new Scene(pane));
+        /*
+         * We use an initial size covering a large fraction of the screen 
because
+         * this window is typically used for showing image or large tabular 
data.
+         */
+        final Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
+        stage.setWidth (0.8 * bounds.getWidth());
+        stage.setHeight(0.8 * bounds.getHeight());
+        return stage;
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
index 67ae30e0a3..fb0e5eff3b 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
@@ -24,7 +24,7 @@
  * @author  Smaniotto Enzo (GSoC)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java
index 8686e46534..12b4662640 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/package-info.java
@@ -29,7 +29,7 @@
  * @author  Smaniotto Enzo (GSoC)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
index 20a6424bf3..4f92ee0a4c 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
@@ -26,7 +26,6 @@ import javafx.event.ActionEvent;
 import javafx.geometry.Insets;
 import javafx.stage.Window;
 import javafx.scene.Node;
-import javafx.scene.Scene;
 import javafx.scene.text.Text;
 import javafx.scene.control.Alert;
 import javafx.scene.control.ContextMenu;
@@ -151,22 +150,6 @@ public final class ExceptionReporter extends Widget {
         return view;
     }
 
-    /**
-     * Returns the window where is located the given JavaFX control.
-     *
-     * @param  control  the JavaFX control for which to get the window.
-     * @return window containing the given control, or {@code null} if none.
-     */
-    private static Window getWindow(final Node control) {
-        if (control != null) {
-            final Scene scene = control.getScene();
-            if (scene != null) {
-                return scene.getWindow();
-            }
-        }
-        return null;
-    }
-
     /**
      * Shows the reporter for the exception that occurred during a task.
      *
@@ -179,7 +162,7 @@ public final class ExceptionReporter extends Widget {
         if (worker instanceof DataStoreOpener) {
             canNotReadFile(owner, ((DataStoreOpener) worker).getFileName(), 
exception);
         } else {
-            show(getWindow(owner), (short) 0, (short) 0, null, exception);
+            show(GUIUtilities.getWindow(owner), (short) 0, (short) 0, null, 
exception);
         }
     }
 
@@ -213,7 +196,8 @@ public final class ExceptionReporter extends Widget {
      * @param  exception  the error that occurred.
      */
     public static void canNotReadFile(final Node owner, final String file, 
final Throwable exception) {
-        show(getWindow(owner), Resources.Keys.ErrorOpeningFile, 
Resources.Keys.CanNotReadFile_1, new Object[] {file}, exception);
+        show(GUIUtilities.getWindow(owner), Resources.Keys.ErrorOpeningFile, 
Resources.Keys.CanNotReadFile_1,
+                new Object[] {file}, exception);
     }
 
     /**
@@ -225,7 +209,8 @@ public final class ExceptionReporter extends Widget {
      * @param  exception  the error that occurred.
      */
     public static void canNotCloseFile(final Node owner, final String file, 
final Throwable exception) {
-        show(getWindow(owner), Resources.Keys.ErrorClosingFile, 
Resources.Keys.CanNotClose_1, new Object[] {file}, exception);
+        show(GUIUtilities.getWindow(owner), Resources.Keys.ErrorClosingFile, 
Resources.Keys.CanNotClose_1,
+                new Object[] {file}, exception);
     }
 
     /**
@@ -237,7 +222,8 @@ public final class ExceptionReporter extends Widget {
      * @param  exception  the error that occurred.
      */
     public static void canNotCreateCRS(final Window owner, final String code, 
final Throwable exception) {
-        show(owner, Resources.Keys.ErrorCreatingCRS, 
Resources.Keys.CanNotCreateCRS_1, new Object[] {code}, exception);
+        show(owner, Resources.Keys.ErrorCreatingCRS, 
Resources.Keys.CanNotCreateCRS_1,
+                new Object[] {code}, exception);
     }
 
     /**
@@ -248,7 +234,8 @@ public final class ExceptionReporter extends Widget {
      * @param  exception  the error that occurred.
      */
     public static void canNotUseResource(final Node owner, final Throwable 
exception) {
-        show(getWindow(owner), Resources.Keys.ErrorDataAccess, 
Resources.Keys.ErrorDataAccess, new Object[0], exception);
+        show(GUIUtilities.getWindow(owner), Resources.Keys.ErrorDataAccess, 
Resources.Keys.ErrorDataAccess,
+                new Object[0], exception);
     }
 
     /**
@@ -285,7 +272,7 @@ public final class ExceptionReporter extends Widget {
      * @param exception  the exception to report.
      */
     public static void show(final Node owner, final String title, final String 
text, final Throwable exception) {
-        show(getWindow(owner), title, text, exception);
+        show(GUIUtilities.getWindow(owner), title, text, exception);
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
index 939570eb0e..8e4d75ed6e 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -102,6 +102,22 @@ public final class GUIUtilities extends Static {
         return null;
     }
 
+    /**
+     * Returns the window where is located the given JavaFX control.
+     *
+     * @param  control  the JavaFX control for which to get the window, or 
{@code null} if none.
+     * @return window containing the given control, or {@code null} if none.
+     */
+    public static Window getWindow(final Node control) {
+        if (control != null) {
+            final Scene scene = control.getScene();
+            if (scene != null) {
+                return scene.getWindow();
+            }
+        }
+        return null;
+    }
+
     /**
      * Sets on the given pane a clip defined to the pane bounds. This method 
is invoked for pane having content
      * that may be drawn outside the pane bounds (typically images). We use 
this method as a workaround for the
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
similarity index 51%
copy from 
application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
copy to 
application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
index 67ae30e0a3..68a15aaab3 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
@@ -14,18 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.sis.internal.gui;
+
+import java.util.function.BiConsumer;
+import org.apache.sis.gui.coverage.CoverageExplorer;
+import org.apache.sis.gui.dataset.WindowHandler;
+
 
 /**
- * Widgets about data store resources and their metadata.
- * Those widgets can show a hierarchical collection of {@link 
org.apache.sis.storage.Resource}s in a tree,
- * and show their content in other panel when a resource is selected.
- * The resources can optionally be loaded from a file in background thread.
+ * Accessor for fields that we want to keep private for now.
+ * This is a way to simulate the behavior of {@code friend} keyword in C++.
+ * Each field shall be set only once in a static block initializer and shall
+ * not be modified after initialization.
  *
- * @author  Smaniotto Enzo (GSoC)
- * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   1.1
+ * @version 1.3
+ * @since   1.3
  * @module
  */
-package org.apache.sis.gui.dataset;
+public final class PrivateAccess {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private PrivateAccess() {
+    }
+
+    /**
+     * A setter method for {@link CoverageExplorer#window}. Shall be invoked 
in JavaFX thread.
+     */
+    public static volatile BiConsumer<CoverageExplorer, WindowHandler> 
initWindowHandler;
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
index bb260cc154..b214c11f90 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
@@ -44,7 +44,7 @@ import static java.util.logging.Logger.getLogger;
  * <p>This class also opportunistically provides a few utility methods related 
to appearance.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -61,11 +61,6 @@ public final class Styles extends Static {
      */
     public static final int SCROLLBAR_WIDTH = 20;
 
-    /**
-     * Width of a checkbox or radio item in a table cell.
-     */
-    public static final int CHECKBOX_WIDTH = 40;
-
     /**
      * "Standard" height of table rows. Can be approximate.
      */
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
index 0c179f35f3..b0730abf3f 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ToolbarButton.java
@@ -27,9 +27,9 @@ import org.apache.sis.util.ArraysExt;
 
 
 /**
- * Builder for a button to add in a the {@link 
org.apache.sis.gui.dataset.DataWindow} toolbar.
- * This class is used only for content-specific buttons; it is not used for 
buttons managed directly by
- * {@code DataWindow} itself. A {@code ToolbarButton} can create and configure 
a button with its icon,
+ * Builder for a button to add in a the toolbar of a {@link 
org.apache.sis.gui.dataset} window.
+ * This class is used only for content-specific buttons; it is not used for 
all buttons created
+ * by the {@code dataset} package. A {@code ToolbarButton} can create and 
configure a button with its icon,
  * tooltip text and action to execute when the button is pushed.
  *
  * <p>This class is defined in this internal package for allowing interactions 
between classes
@@ -50,7 +50,7 @@ public abstract class ToolbarButton implements 
EventHandler<ActionEvent> {
     /**
      * Gets and removes the toolbar buttons associated to the given content 
pane. Those buttons
      * should have been specified by a previous call to {@link #insert(Node, 
Control...)}.
-     * They will be requested by {@link org.apache.sis.gui.dataset.DataWindow} 
only once,
+     * They will be requested by {@link 
org.apache.sis.gui.dataset.WindowHandler} only once,
      * which is why we remove them afterward.
      *
      * @param  content  the pane for which to get the toolbar buttons.
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
index 2e8a95d22e..60b6c90016 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
@@ -36,7 +36,7 @@ import org.apache.sis.internal.gui.ImmutableObjectProperty;
  * that may change in any future version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @param  <S>  the type of row data as declared in the {@link TableView} 
generic type.
  *
@@ -143,6 +143,5 @@ public abstract class ColorColumnHandler<S> implements 
Callback<TableColumn.Cell
         });
         table.getColumns().add(colors);
         table.setEditable(true);
-        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
new file mode 100644
index 0000000000..dfc5c6a281
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
@@ -0,0 +1,184 @@
+/*
+ * 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.internal.gui.control;
+
+import java.util.List;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Button;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.gui.dataset.WindowHandler;
+import org.apache.sis.gui.map.MapCanvas;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+
+
+/**
+ * Provides a widget for listing all available windows and selecting the ones 
to follow
+ * on gesture events (zoom, pans, <i>etc</i>).
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public final class SyncWindowList extends TabularWidget implements 
ListChangeListener<WindowHandler> {
+    /**
+     * Window containing a {@link MapCanvas} to follow on gesture events.
+     * Gestures are followed only if {@link #linked} is {@code true}.
+     */
+    private static final class Link {
+        /**
+         * Whether the "foreigner" {@linkplain #view} should be followed.
+         */
+        public final BooleanProperty linked;
+
+        /**
+         * The "foreigner" view for which to follow the gesture.
+         */
+        public final WindowHandler view;
+
+        /**
+         * Creates a new row for a window to follow.
+         *
+         * @param  view  the "foreigner" view for which to follow the gesture.
+         */
+        private Link(final WindowHandler view) {
+            this.view = view;
+            linked = new SimpleBooleanProperty(this, "linked");
+        }
+
+        /**
+         * Converts the given list of handled to a list of table rows.
+         *
+         * @param  added    list of new items to put in the table.
+         * @param  exclude  item to exclude (because the referenced window is 
itself).
+         */
+        static List<Link> wrap(final List<? extends WindowHandler> added, 
final WindowHandler exclude) {
+            final Link[] items = new Link[added.size()];
+            int count = 0;
+            for (final WindowHandler view : added) {
+                if (view != exclude) {
+                    items[count++] = new Link(view);
+                }
+            }
+            return UnmodifiableArrayList.wrap(items, 0, count);
+        }
+    }
+
+    /**
+     * The table showing values associated to colors.
+     */
+    private final TableView<Link> table;
+
+    /**
+     * The button for creating a new window.
+     */
+    private final Button newWindow;
+
+    /**
+     * The view for which to create a list of synchronized windows.
+     */
+    private final WindowHandler owner;
+
+    /**
+     * The component to be returned by {@link #getView()}.
+     */
+    private final VBox content;
+
+    /**
+     * Creates a new "synchronized windows" widget.
+     *
+     * @param  owner       the view for which to create a list of synchronized 
windows.
+     * @param  resources   localized resources, given because already known by 
the caller.
+     * @param  vocabulary  localized resources, given because already known by 
the caller
+     *                     (those arguments would be removed if this 
constructor was public API).
+     */
+    @SuppressWarnings("ThisEscapedInObjectConstruction")
+    public SyncWindowList(final WindowHandler owner, final Resources 
resources, final Vocabulary vocabulary) {
+        this.owner = owner;
+        table = newTable();
+        newWindow = new Button(resources.getString(Resources.Keys.NewWindow));
+        newWindow.setMaxWidth(Double.MAX_VALUE);
+        /*
+         * The first column contains a checkbox for choosing whether the 
window should be followed or not.
+         * Header text is 🔗 (link symbol).
+         */
+        final TableColumn<Link,Boolean> linked = 
newBooleanColumn("\uD83D\uDD17", (cell) -> cell.getValue().linked);
+        final TableColumn<Link,String> name = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Title));
+        name.setCellValueFactory((cell) -> cell.getValue().view.title);
+        table.getColumns().setAll(linked, name);
+        table.getItems().setAll(Link.wrap(owner.manager.windows, owner));
+        /*
+         * Build all other widget controls.
+         */
+        newWindow.setOnAction((e) -> owner.duplicate().show());
+        VBox.setVgrow(table, Priority.ALWAYS);
+        VBox.setVgrow(newWindow, Priority.NEVER);
+        content = new VBox(9, table, newWindow);
+        /*
+         * Add listener last when the everything else is successful
+         * (because the `this` reference escapes).
+         */
+        owner.manager.windows.addListener(this);
+    }
+
+    /**
+     * Returns the encapsulated JavaFX component to add in a scene graph for 
making the table visible.
+     * The {@code Region} subclass is implementation dependent and may change 
in any future SIS version.
+     *
+     * @return the JavaFX component to insert in a scene graph.
+     */
+    @Override
+    public Region getView() {
+        return content;
+    }
+
+    /**
+     * Invoked when new items are added or removed in the list of windows.
+     *
+     * @param  change  a description of changes in the list of windows.
+     */
+    @Override
+    public void onChanged(final Change<? extends WindowHandler> change) {
+        final ObservableList<Link> items = table.getItems();
+        while (change.next()) {
+            // Ignore permutations; each table can have its own order.
+            if (change.wasRemoved()) {
+                // List of removed items usually has a single element.
+                for (final WindowHandler item : change.getRemoved()) {
+                    for (int i = items.size(); --i >= 0;) {
+                        if (items.get(i).view == item) {
+                            items.remove(i);
+                            break;
+                        }
+                    }
+                }
+            }
+            if (change.wasAdded()) {
+                items.addAll(Link.wrap(change.getAddedSubList(), owner));
+            }
+        }
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/TabularWidget.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/TabularWidget.java
new file mode 100644
index 0000000000..1b204f5f8e
--- /dev/null
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/TabularWidget.java
@@ -0,0 +1,80 @@
+/*
+ * 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.internal.gui.control;
+
+import javafx.util.Callback;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumn.CellDataFeatures;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.CheckBoxTableCell;
+import org.apache.sis.gui.Widget;
+
+
+/**
+ * Base class of widgets providing a table of something.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+abstract class TabularWidget extends Widget {
+    /**
+     * Width of a checkbox or radio item in a table cell.
+     */
+    private static final int CHECKBOX_WIDTH = 40;
+
+    /**
+     * Creates a new widget.
+     */
+    TabularWidget() {
+    }
+
+    /**
+     * Creates an initially empty table.
+     *
+     * @param  <S>  the type of objects contained within the {@link TableView} 
items list.
+     * @return the initially empty table.
+     */
+    static <S> TableView<S> newTable() {
+        TableView<S> table = new TableView<>();
+        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        return table;
+    }
+
+    /**
+     * Creates a new column for a boolean value represented by a checkbox.
+     *
+     * @param  <S>      the type of objects contained within the {@link 
TableView} items list.
+     * @param  header   column header.
+     * @param  factory  link to the boolean property.
+     * @return a column for checkbox.
+     */
+    static <S> TableColumn<S,Boolean> newBooleanColumn(final String header,
+            final Callback<CellDataFeatures<S,Boolean>, 
ObservableValue<Boolean>> factory)
+    {
+        final TableColumn<S,Boolean> c = new TableColumn<>(header);
+        c.setCellFactory(CheckBoxTableCell.forTableColumn(c));
+        c.setCellValueFactory(factory);
+        c.setSortable(false);
+        c.setResizable(false);
+        c.setMinWidth(CHECKBOX_WIDTH);
+        c.setMaxWidth(CHECKBOX_WIDTH);
+        return c;
+    }
+}
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
index 341a1ba228..b6a27669dd 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
@@ -43,12 +43,10 @@ import javafx.scene.control.MenuItem;
 import javafx.scene.control.TextField;
 import javafx.scene.control.TableView;
 import javafx.scene.control.TableColumn;
-import javafx.scene.control.cell.CheckBoxTableCell;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.gui.Widget;
 
 
 /**
@@ -56,11 +54,11 @@ import org.apache.sis.gui.Widget;
  * It can be used as a table of isolines or as a {@link ColorRamp} editor.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
-public final class ValueColorMapper extends Widget {
+public final class ValueColorMapper extends TabularWidget {
     /**
      * Colors to associate to a given value.
      *
@@ -189,8 +187,9 @@ public final class ValueColorMapper extends Widget {
      *                     (those arguments would be removed if this 
constructor was public API).
      */
     public ValueColorMapper(final Resources resources, final Vocabulary 
vocabulary) {
+        table = newTable();
         textConverter = FormatApplicator.createNumberFormat();
-        table = createIsolineTable(vocabulary);
+        createIsolineTable(vocabulary);
         final MenuItem rangeMenu = new 
MenuItem(resources.getString(Resources.Keys.RangeOfValues));
         final MenuItem clearAll  = new 
MenuItem(resources.getString(Resources.Keys.ClearAll));
         rangeMenu.setOnAction((e) -> insertRangeOfValues());
@@ -362,20 +361,13 @@ public final class ValueColorMapper extends Widget {
      *
      * @param  vocabulary  localized resources, given because already known by 
the caller
      *                     (this argument would be removed if this method was 
public API).
-     * @return table of isolines.
      */
-    private TableView<Step> createIsolineTable(final Vocabulary vocabulary) {
+    private void createIsolineTable(final Vocabulary vocabulary) {
         /*
          * First column containing a checkbox for choosing whether the isoline 
should be drawn or not.
          * Header text is 🖉 (lower left pencil).
          */
-        final TableColumn<Step,Boolean> visible = new 
TableColumn<>("\uD83D\uDD89");
-        visible.setCellFactory(CheckBoxTableCell.forTableColumn(visible));
-        visible.setCellValueFactory((cell) -> cell.getValue().visible);
-        visible.setSortable(false);
-        visible.setResizable(false);
-        visible.setMinWidth(Styles.CHECKBOX_WIDTH);
-        visible.setMaxWidth(Styles.CHECKBOX_WIDTH);
+        final TableColumn<Step,Boolean> visible = 
newBooleanColumn("\uD83D\uDD89", (cell) -> cell.getValue().visible);
         /*
          * Second column containing the level value.
          * The number can be edited using a `NumberFormat` in current locale.
@@ -388,10 +380,9 @@ public final class ValueColorMapper extends Widget {
         level.setSortable(false);                               // We will do 
our own sorting.
         level.setId("level");
         /*
-         * Create the table with above "category name" column (read-only),
+         * Create the table with above "levels" column (read-only),
          * and add an editable column for color(s).
          */
-        final TableView<Step> table = new TableView<>();
         table.getColumns().setAll(visible, level);
         final ColumnHandler handler = new ColumnHandler();
         handler.addColumnTo(table, 
vocabulary.getString(Vocabulary.Keys.Color));
@@ -400,10 +391,9 @@ public final class ValueColorMapper extends Widget {
          * when a digit is typed (this is the purpose of `trigger`). For 
making easier to edit the cell in current row,
          * a listener on F2 key (same as Excel and OpenOffice) is also 
registered.
          */
-        table.getItems().add(new Step());
+        getSteps().add(new Step());
         trigger.registerTo(table);
         table.setOnKeyPressed(ValueColorMapper::deleteRow);
-        return table;
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
index 7366f0773b..d77f9da70b 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
@@ -24,7 +24,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index ae677139d3..f7f2fe3b5b 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -1229,6 +1229,11 @@ public final class Vocabulary extends 
IndexedResourceBundle {
          */
         public static final short Timezone = 197;
 
+        /**
+         * Title
+         */
+        public static final short Title = 270;
+
         /**
          * Topic category
          */
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index e91653a67a..1ad786ab8e 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -249,6 +249,7 @@ TileSize                = Tile size
 Time                    = Time
 Time_1                  = {0} time
 Timezone                = Timezone
+Title                   = Title
 TopicCategory           = Topic category
 Trace                   = Trace
 Transformation          = Transformation
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index b1c43ab222..4bff86e19e 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -244,10 +244,10 @@ SpatialRepresentation   = Repr\u00e9sentation spatiale
 StandardDeviation       = \u00c9cart type
 StartDate               = Date de d\u00e9part
 StartPoint              = Point de d\u00e9part
+Stretching              = \u00c9tirement
 SubsetOf_1              = Sous-ensemble de {0}
 Summary                 = R\u00e9sum\u00e9
 SupersededBy_1          = Remplac\u00e9 par {0}.
-Stretching              = \u00c9tirement
 Temporal                = Temporel
 TemporalExtent          = Plage temporelle
 TemporaryFiles          = Fichiers temporaires
@@ -256,6 +256,7 @@ TileSize                = Taille des tuiles
 Time                    = Temps
 Time_1                  = Heure {0}
 Timezone                = Fuseau horaire
+Title                   = Titre
 TopicCategory           = Cat\u00e9gorie th\u00e9matique
 Trace                   = Trace
 Transformation          = Transformation

Reply via email to