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.
     }
 

Reply via email to