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 2c94ea0468 When a resource is closed, all windows showing that
resource should be closed as well.
2c94ea0468 is described below
commit 2c94ea046838730831410d8421a9239bc8d74f15
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jun 8 18:44:30 2022 +0200
When a resource is closed, all windows showing that resource should be
closed as well.
---
.../org/apache/sis/gui/dataset/ResourceEvent.java | 2 +-
.../org/apache/sis/gui/dataset/WindowHandler.java | 97 +++++++++++++++++++---
.../sis/storage/event/CascadedStoreEvent.java | 5 +-
.../apache/sis/storage/event/StoreListeners.java | 40 ++++++++-
.../sis/storage/event/StoreListenersTest.java | 3 +
5 files changed, 132 insertions(+), 15 deletions(-)
diff --git
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceEvent.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceEvent.java
index fdd75cb817..915e34d9fd 100644
---
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceEvent.java
+++
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceEvent.java
@@ -64,7 +64,7 @@ public class ResourceEvent extends Event {
* Creates a new event.
*
* @param source the source of this event.
- * @param path path to the file being loaded.
+ * @param path path to the file being opened or closed.
* @param type the type of event.
*/
ResourceEvent(final ResourceTree source, final Path path, final
EventType<ResourceEvent> type) {
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
index d324cdb311..07289bfe22 100644
---
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
@@ -18,16 +18,23 @@ package org.apache.sis.gui.dataset;
import java.util.Locale;
import java.util.logging.Logger;
+import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.event.EventHandler;
import javafx.scene.layout.Region;
import javafx.beans.value.ChangeListener;
import javafx.beans.property.StringProperty;
import javafx.beans.property.SimpleStringProperty;
+import javafx.collections.ObservableList;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.CloseEvent;
+import org.apache.sis.storage.event.StoreListener;
import org.apache.sis.gui.coverage.CoverageExplorer;
+import org.apache.sis.internal.gui.BackgroundThreads;
import org.apache.sis.internal.gui.GUIUtilities;
import org.apache.sis.internal.gui.PrivateAccess;
import org.apache.sis.internal.gui.Resources;
@@ -150,6 +157,14 @@ public abstract class WindowHandler {
*/
public abstract WindowHandler duplicate();
+ /**
+ * 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();
+
/**
* Returns the JavaFX region where the resource is shown. This value shall
be stable.
*/
@@ -182,11 +197,14 @@ public abstract class WindowHandler {
return;
}
} else {
- if (getResource() == null) {
+ final Resource resource = getResource();
+ if (resource == null) {
throw new
IllegalStateException(Errors.format(Errors.Keys.DisposedInstanceOf_1,
getClass()));
}
+ final OnClose listener = new OnClose();
+ resource.addListener(CloseEvent.class, listener);
window = manager.newWindow(getView());
- window.setOnHidden((e) -> dispose());
+ window.setOnHidden(listener);
title.addListener(TITLE_CHANGED);
TITLE_CHANGED.changed(title, null, title.get());
}
@@ -196,20 +214,79 @@ public abstract class WindowHandler {
}
/**
- * 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.
+ * The action to execute when a window is closed or when the resource
shown in windows is closed.
+ * When the event is a window closing, it impacts only that window. When
the event is a resource
+ * closing, it impacts all windows showing that resource.
*/
- abstract Resource getResource();
+ private final class OnClose implements StoreListener<CloseEvent>,
EventHandler<WindowEvent> {
+ /**
+ * Creates a new listener to be notified when a window or a resource
is closed.
+ */
+ OnClose() {
+ }
+
+ /**
+ * Invoked in JavaFX thread when the window is closing.
+ */
+ @Override public void handle(final WindowEvent event) {
+ manager.modifiableWindowList.remove(WindowHandler.this);
+ final Resource resource = getResource();
+ if (resource != null) {
+ resource.removeListener(CloseEvent.class, this);
+ }
+ dispose();
+ }
+
+ /**
+ * Invoked when a resource is closing. This method can be invoked from
any thread, but the actual work
+ * will done in the JavaFX thread. If the {@link CloseEvent} is fired
from a background thread, then
+ * this method will block until the JavaFX thread finished to close
all windows, for avoiding that
+ * the background thread closes the resource before we removed all
usages in JavaFX thread.
+ */
+ @Override public void eventOccured(final CloseEvent event) {
+ final Resource resource = event.getSource();
+ if (resource != null) {
+ resource.removeListener(CloseEvent.class, this);
+ }
+ if (Platform.isFxApplicationThread()) {
+ close(resource, WindowHandler.this);
+ } else BackgroundThreads.runAndWaitDialog(() -> {
+ close(resource, WindowHandler.this);
+ return null;
+ });
+ }
+ }
+
+ /**
+ * Closes all windows (except the main window) which are showing the given
resource.
+ * This is invoked when the resource has been closed in the {@link
ResourceTree}.
+ *
+ * @param resource the resource which has been closed.
+ * @param force the handler to close unconditionally regardless its
resource.
+ */
+ private static void close(final Resource resource, final WindowHandler
force) {
+ final WindowHandler ignore = force.manager.main;
+ final ObservableList<WindowHandler> windows =
force.manager.modifiableWindowList;
+ for (int i = windows.size(); --i >= 0;) {
+ final WindowHandler handler = windows.get(i);
+ if (handler != ignore && (handler == force ||
handler.getResource() == resource)) {
+ windows.remove(i);
+ if (handler.window != null) {
+ handler.window.setOnHidden(null); // Because we will
invoke `dispose()` ourselves.
+ handler.window.close();
+ }
+ handler.dispose();
+ }
+ }
+ }
/**
- * 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.
+ * Makes a "best effort" for helping the garbage-collector to release
memory.
+ * This method is for internal usage by {@code WindowHandler} and
subclasses only.
+ * Caller shall remove this handler from the windows list before to invoke
this method.
*/
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);
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
index 6a2c82f09a..cf3823f26e 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
@@ -62,7 +62,6 @@ public abstract class CascadedStoreEvent<E extends
CascadedStoreEvent<E>> extend
*/
protected CascadedStoreEvent(Resource source) {
super(source);
- super.consume(true); // Necessary for avoiding never-ending
loop.
}
/**
@@ -121,7 +120,9 @@ public abstract class CascadedStoreEvent<E extends
CascadedStoreEvent<E>> extend
if (r == null) {
parent.removeListener(eventType, this);
} else try {
- StoreListeners.fire(r, eventType,
event.forSource(r.getSource()));
+ final E cascade = event.forSource(r.getSource());
+ cascade.consume(true); // For avoiding never-ending
loop.
+ StoreListeners.fire(r, eventType, cascade);
} catch (ExecutionException e) {
StoreListeners.canNotNotify("fire (cascade)", e);
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
index ed970aa49e..91bb6d4f76 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
@@ -228,6 +228,13 @@ public class StoreListeners implements Localized {
}
}
+ /**
+ * Returns {@code true} if this element contains the given listener.
+ */
+ final boolean hasListener(final StoreListener<?> listener) {
+ return ArraysExt.containsIdentity(listeners, listener);
+ }
+
/**
* Returns {@code true} if this element has at least one listener.
*/
@@ -741,7 +748,9 @@ public class StoreListeners implements Localized {
* @see Resource#addListener(Class, StoreListener)
*/
@SuppressWarnings({"rawtypes","unchecked"})
- public synchronized <E extends StoreEvent> void addListener(final Class<E>
eventType, final StoreListener<? super E> listener) {
+ public synchronized <E extends StoreEvent> void addListener(
+ final Class<E> eventType, final StoreListener<? super E> listener)
+ {
ArgumentChecks.ensureNonNull("listener", listener);
ArgumentChecks.ensureNonNull("eventType", eventType);
if (isPossibleEvent(permittedEventTypes, eventType)) {
@@ -798,7 +807,9 @@ public class StoreListeners implements Localized {
* @see Resource#removeListener(Class, StoreListener)
*/
@SuppressWarnings({"rawtypes","unchecked"})
- public synchronized <E extends StoreEvent> void removeListener(Class<E>
eventType, StoreListener<? super E> listener) {
+ public synchronized <E extends StoreEvent> void removeListener(
+ final Class<E> eventType, final StoreListener<? super E> listener)
+ {
ArgumentChecks.ensureNonNull("listener", listener);
ArgumentChecks.ensureNonNull("eventType", eventType);
for (ForType<?> e = listeners; e != null; e = e.next) {
@@ -814,6 +825,31 @@ public class StoreListeners implements Localized {
}
}
+ /**
+ * Returns {@code true} if the given listener is registered for the given
type or a super-type.
+ * This method may unconditionally return {@code false} if the given type
of event is never fired
+ * by this {@code StoreListeners}, because calls to {@code
addListener(eventType, …)} are free to
+ * ignore the listeners for those types.
+ *
+ * @param <E> compile-time value of the {@code eventType} argument.
+ * @param eventType type of {@link StoreEvent} to check (can not be
{@code null}).
+ * @param listener listener to check for registration.
+ * @return {@code true} if this object contains the specified listener for
given event type, {@code false} otherwise.
+ *
+ * @since 1.3
+ */
+ public <E extends StoreEvent> boolean hasListener(final Class<E>
eventType, final StoreListener<? super E> listener) {
+ // No need to synchronize this method.
+ ArgumentChecks.ensureNonNull("listener", listener);
+ ArgumentChecks.ensureNonNull("eventType", eventType);
+ for (ForType<?> e = listeners; e != null; e = e.next) {
+ if (e.type.equals(eventType) && e.hasListener(listener)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns {@code true} if at least one listener is registered for the
given type or a super-type.
* This method may unconditionally return {@code false} if the given type
of event is never fired
diff --git
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
index 140b54a305..dbf3f176bb 100644
---
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
+++
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
@@ -74,10 +74,13 @@ public final strictfp class StoreListenersTest extends
TestCase implements Store
public void testAddAndRemoveStoreListener() {
final StoreListeners listeners = store.listeners();
assertFalse("hasListeners()",
listeners.hasListeners(WarningEvent.class));
+ assertFalse("hasListener(…)", listeners.hasListener
(WarningEvent.class, this));
listeners.addListener(WarningEvent.class, this);
assertTrue("hasListeners()",
listeners.hasListeners(WarningEvent.class));
+ assertTrue("hasListener(…)", listeners.hasListener
(WarningEvent.class, this));
listeners.removeListener(WarningEvent.class, this);
assertFalse("hasListeners()",
listeners.hasListeners(WarningEvent.class));
+ assertFalse("hasListener(…)", listeners.hasListener
(WarningEvent.class, this));
listeners.removeListener(WarningEvent.class, this); // Should
be no-op.
}