Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Window.PreCloseListener that can be used to prevent window close #10535

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
182 changes: 168 additions & 14 deletions server/src/main/java/com/vaadin/ui/Window.java
Expand Up @@ -16,18 +16,7 @@

package com.vaadin.ui;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.vaadin.event.ConnectorEvent;
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
Expand All @@ -36,6 +25,7 @@
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.event.FieldEvents.FocusNotifier;
import com.vaadin.event.MouseEvents.ClickEvent;
import com.vaadin.event.SerializableEventListener;
import com.vaadin.event.ShortcutAction;
import com.vaadin.event.ShortcutAction.KeyCode;
import com.vaadin.event.ShortcutAction.ModifierKey;
Expand All @@ -54,6 +44,19 @@
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.util.ReflectTools;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* A component that represents a floating popup window that can be added to a
Expand Down Expand Up @@ -110,6 +113,8 @@ public void windowMoved(int x, int y) {
*/
private List<CloseShortcut> closeShortcuts = new ArrayList<>(4);

private LinkedHashSet<WindowBeforeCloseListener> beforeCloseListeners = null;

/**
* Used to keep the window order position. Order position for unattached
* window is {@code -1}.
Expand Down Expand Up @@ -231,7 +236,7 @@ public void changeVariables(Object source, Map<String, Object> variables) {
// Closing
final Boolean close = (Boolean) variables.get("close");
if (close != null && close.booleanValue()) {
close();
closeFromClient();
}
}

Expand All @@ -248,6 +253,49 @@ public void changeVariables(Object source, Map<String, Object> variables) {

}

/**
* Fires an event before window close.
* <p>
* Listeners are called in registration order. If any listener returns
* <code>false</code>, the rest of the listeners are not called and the
* window close is blocked.
* <p>
* The window close listeners may also e.g. open a warning or question
* dialog and save the parameters to re-initiate the close operation upon
* user action.
*
* @param event
* window close event (not null, window close not yet performed)
* @return true if the window close should be allowed, false to silently
* block the close operation
* @since
*/
protected boolean fireBeforeWindowClose(WindowBeforeCloseEvent event) {
// a copy of the listener list is needed to avoid
// ConcurrentModificationException as a listener can add/remove
// listeners
if (beforeCloseListeners != null) {
for (WindowBeforeCloseListener l : new ArrayList<>(beforeCloseListeners)) {
if (!l.beforeWindowClose(event)) {
return false;
}
}
}
return true;
}

/**
* Called when client tries to close the window.
*
* @since
*/
protected void closeFromClient() {
WindowBeforeCloseEvent event = new WindowBeforeCloseEvent(this);
if (fireBeforeWindowClose(event)) {
close();
}
}

/**
* Method that handles window closing (from UI).
*
Expand Down Expand Up @@ -735,6 +783,88 @@ protected void fireResize() {
fireEvent(new ResizeEvent(this));
}

/**
* WindowBeforeCloseEvent is fired whenever the user closes window using close
* button or triggers close shortcut. The event is not triggered on
* {@link UI#removeWindow(Window)} call.
*
* @since
*/
public static class WindowBeforeCloseEvent extends Component.Event {
public WindowBeforeCloseEvent(Window window) {
super(window);
}

/**
* Gets the Window.
*
* @return the window.
*/
public Window getWindow() {
return (Window) getSource();
}
}

/**
* An interface used for listening to Window close events. Add the
* WindowBeforeCloseListener to a window and
* {@link WindowBeforeCloseListener#beforeWindowClose(WindowBeforeCloseEvent)}
* will be called whenever the user closes the window using close button or
* close shortcut.
*
* <p>
* Implementation of the listener may cancel window close using
* {@link WindowBeforeCloseEvent#setClosePrevented(boolean)} method call.
* </p>
*
* @since
*/
@FunctionalInterface
public interface WindowBeforeCloseListener extends SerializableEventListener {
/**
* Called when the window is about to be removed from UI due to close
* attempt from client.
* <p>
* If this listener does not want to block the window close, it should
* return true. If any listener returns false, the window close is not
* allowed.
*
* @param event
* window close attempt event
* @return true if the window close should be allowed or this listener
* does not care about the window close, false to block the
* close
*/
public boolean beforeWindowClose(WindowBeforeCloseEvent event);
}

/**
* Adds a WindowBeforeCloseListener to the window.
*
* For a window the WindowBeforeCloseListener is fired when the user closes
* it (clicks on the close button or uses close shortcut).
*
* @param listener
* the WindowBeforeCloseListener to add, not null
* @since
*/
public Registration addWindowBeforeCloseListener(WindowBeforeCloseListener listener) {
if (beforeCloseListeners == null) {
beforeCloseListeners = new LinkedHashSet<>();
}

beforeCloseListeners.add(listener);

return () -> {
if (beforeCloseListeners != null) {
beforeCloseListeners.remove(listener);
if (beforeCloseListeners.isEmpty()) {
beforeCloseListeners = null;
}
}
};
}

/**
* Used to keep the right order of windows if multiple windows are brought
* to front in a single changeset. If this is not used, the order is quite
Expand Down Expand Up @@ -1140,7 +1270,7 @@ public CloseShortcut(Window window, int keyCode) {
@Override
public void handleAction(Object sender, Object target) {
if (window.isClosable()) {
window.close();
window.closeFromClient();
}
}

Expand Down Expand Up @@ -1545,4 +1675,28 @@ protected Collection<String> getCustomAttributes() {
result.add("close-shortcut");
return result;
}

@Override
public Collection<?> getListeners(Class<?> eventType) {
if (WindowBeforeCloseEvent.class.isAssignableFrom(eventType)) {
if (beforeCloseListeners == null) {
return Collections.EMPTY_LIST;
} else {
return Collections
.unmodifiableCollection(beforeCloseListeners);
}
} else if (ConnectorEvent.class.isAssignableFrom(eventType)
|| Component.Event.class.isAssignableFrom(eventType)) {
if (beforeCloseListeners == null) {
return super.getListeners(eventType);
} else {
Set<Object> listeners = new LinkedHashSet<>();
listeners.addAll(beforeCloseListeners);
listeners.addAll(super.getListeners(eventType));
return listeners;
}
}

return super.getListeners(eventType);
}
}
@@ -1,17 +1,13 @@
package com.vaadin.tests.server.component.window;

import org.junit.Test;

import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.FocusEvent;
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase;
import com.vaadin.ui.Window;
import com.vaadin.ui.Window.CloseEvent;
import com.vaadin.ui.Window.CloseListener;
import com.vaadin.ui.Window.ResizeEvent;
import com.vaadin.ui.Window.ResizeListener;
import com.vaadin.ui.Window.*;
import org.junit.Test;

public class WindowListenersTest extends AbstractListenerMethodsTestBase {

Expand All @@ -38,4 +34,10 @@ public void testCloseListenerAddGetRemove() throws Exception {
testListenerAddGetRemove(Window.class, CloseEvent.class,
CloseListener.class);
}

@Test
public void testBeforeCloseListenerAddGetRemove() throws Exception {
testListenerAddGetRemove(Window.class, WindowBeforeCloseEvent.class,
WindowBeforeCloseListener.class);
}
}
@@ -1,19 +1,19 @@
package com.vaadin.tests.server.components;

import java.util.HashMap;
import java.util.Map;

import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.shared.Registration;
import com.vaadin.ui.LegacyWindow;
import com.vaadin.ui.Window;
import com.vaadin.ui.Window.CloseEvent;
import com.vaadin.ui.Window.CloseListener;
import com.vaadin.ui.Window.ResizeEvent;
import com.vaadin.ui.Window.ResizeListener;
import com.vaadin.ui.Window.WindowBeforeCloseListener;
import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

public class WindowTest {

Expand Down Expand Up @@ -53,6 +53,34 @@ public void testCloseListener() {

}

@Test
public void testBeforeCloseListener() {
WindowBeforeCloseListener pcl = EasyMock.createMock(WindowBeforeCloseListener.class);

// Expectations
EasyMock.expect(pcl.beforeWindowClose(EasyMock.isA(Window
.WindowBeforeCloseEvent.class))).andStubReturn(true);

// Start actual test
EasyMock.replay(pcl);

// Add listener and send a close event -> should end up in listener once
Registration windowPreCloseListenerRegistration = window
.addWindowBeforeCloseListener(pcl);
sendClose(window);

// Ensure listener was called once
EasyMock.verify(pcl);

// Remove the listener and send close event -> should not end up in
// listener
windowPreCloseListenerRegistration.remove();
sendClose(window);

// Ensure listener still has been called only once
EasyMock.verify(pcl);
}

@Test
public void testResizeListener() {
ResizeListener rl = EasyMock.createMock(Window.ResizeListener.class);
Expand Down
@@ -0,0 +1,53 @@
package com.vaadin.tests.components.window;

import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.components.AbstractTestUIWithLog;
import com.vaadin.ui.Button;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;

public class WindowBeforeCloseListener extends AbstractTestUIWithLog {

@Override
protected void setup(VaadinRequest request) {
Button openWindowButton = new Button("Open sub-window");
openWindowButton.setId("opensub");
openWindowButton.addClickListener(event -> {
Window sub = createClosableSubWindow("Sub-window");
getUI().addWindow(sub);
});

addComponent(openWindowButton);
}

private Window createClosableSubWindow(final String title) {
VerticalLayout layout = new VerticalLayout();
layout.setMargin(true);
layout.setSizeUndefined();
final Window window = new Window(title, layout);
window.setSizeUndefined();
window.setClosable(true);

Button closeButton = new Button("Close");
closeButton.addClickListener(
event -> event.getButton().findAncestor(Window.class).close());
layout.addComponent(closeButton);

window.addCloseListener(event -> {
log("Window '" + title + "' closed");
});

window.addWindowBeforeCloseListener(event -> {
log("Window '" + title + "' close attempt prevented");

return false;
});

return window;
}

@Override
protected String getTestDescription() {
return "Try to close window both from code and from client side, and check for close events when WindowBeforeCloseListener prevents closing.";
}
}