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