com.vaadin.ui.UI Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.ui;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.vaadin.annotations.PreserveOnRefresh;
import com.vaadin.event.Action;
import com.vaadin.event.Action.Handler;
import com.vaadin.event.ActionManager;
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.event.MouseEvents.ClickEvent;
import com.vaadin.event.MouseEvents.ClickListener;
import com.vaadin.event.UIEvents.PollEvent;
import com.vaadin.event.UIEvents.PollListener;
import com.vaadin.event.UIEvents.PollNotifier;
import com.vaadin.navigator.Navigator;
import com.vaadin.navigator.PushStateNavigation;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.ComponentSizeValidator.InvalidLayout;
import com.vaadin.server.DefaultErrorHandler;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.ErrorHandlingRunnable;
import com.vaadin.server.LocaleService;
import com.vaadin.server.Page;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.server.UIProvider;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinServlet;
import com.vaadin.server.VaadinSession;
import com.vaadin.server.VaadinSession.State;
import com.vaadin.server.communication.AtmospherePushConnection;
import com.vaadin.server.communication.PushConnection;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.Registration;
import com.vaadin.shared.communication.PushMode;
import com.vaadin.shared.ui.DelayedCallbackRpc;
import com.vaadin.shared.ui.WindowOrderRpc;
import com.vaadin.shared.ui.ui.DebugWindowClientRpc;
import com.vaadin.shared.ui.ui.DebugWindowServerRpc;
import com.vaadin.shared.ui.ui.PageClientRpc;
import com.vaadin.shared.ui.ui.ScrollClientRpc;
import com.vaadin.shared.ui.ui.UIClientRpc;
import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.shared.ui.ui.UIServerRpc;
import com.vaadin.shared.ui.ui.UIState;
import com.vaadin.ui.Component.Focusable;
import com.vaadin.ui.Dependency.Type;
import com.vaadin.ui.Window.WindowOrderChangeListener;
import com.vaadin.ui.declarative.Design;
import com.vaadin.ui.dnd.DragSourceExtension;
import com.vaadin.ui.dnd.DropTargetExtension;
import com.vaadin.util.ConnectorHelper;
import com.vaadin.util.CurrentInstance;
import com.vaadin.util.ReflectTools;
/**
* The topmost component in any component hierarchy. There is one UI for every
* Vaadin instance in a browser window. A UI may either represent an entire
* browser window (or tab) or some part of a html page where a Vaadin
* application is embedded.
*
* The UI is the server side entry point for various client side features that
* are not represented as components added to a layout, e.g notifications, sub
* windows, and executing javascript in the browser.
*
*
* When a new UI instance is needed, typically because the user opens a URL in a
* browser window which points to e.g. {@link VaadinServlet}, all
* {@link UIProvider}s registered to the current {@link VaadinSession} are
* queried for the UI class that should be used. The selection is by default
* based on the UI
init parameter from web.xml.
*
*
* After a UI has been created by the application, it is initialized using
* {@link #init(VaadinRequest)}. This method is intended to be overridden by the
* developer to add components to the user interface and initialize
* non-component functionality. The component hierarchy must be initialized by
* passing a {@link Component} with the main layout or other content of the view
* to {@link #setContent(Component)} or to the constructor of the UI.
*
*
* @see #init(VaadinRequest)
* @see UIProvider
*
* @since 7.0
*/
public abstract class UI extends AbstractSingleComponentContainer
implements Action.Notifier, PollNotifier, LegacyComponent, Focusable {
/**
* The application to which this UI belongs
*/
private volatile VaadinSession session;
/**
* List of windows in this UI.
*/
private final LinkedHashSet windows = new LinkedHashSet<>();
/**
* The component that should be scrolled into view after the next repaint.
* Null if nothing should be scrolled into view.
*/
private Component scrollIntoView;
/**
* The id of this UI, used to find the server side instance of the UI form
* which a request originates. A negative value indicates that the UI id has
* not yet been assigned by the Application.
*
* @see VaadinSession#getNextUIid()
*/
private int uiId = -1;
/**
* Keeps track of the Actions added to this component, and manages the
* painting and handling as well.
*/
protected ActionManager actionManager;
private ConnectorTracker connectorTracker = new ConnectorTracker(this);
private Page page = new Page(this, getState(false).pageState);
private LoadingIndicatorConfiguration loadingIndicatorConfiguration = new LoadingIndicatorConfigurationImpl(
this);
/**
* Scroll Y position.
*/
private int scrollTop = 0;
/**
* Scroll X position
*/
private int scrollLeft = 0;
private UIServerRpc rpc = new UIServerRpc() {
@Override
public void click(MouseEventDetails mouseDetails) {
fireEvent(new ClickEvent(UI.this, mouseDetails));
}
@Override
public void resize(int viewWidth, int viewHeight, int windowWidth,
int windowHeight) {
// TODO We're not doing anything with the view dimensions
getPage().updateBrowserWindowSize(windowWidth, windowHeight, true);
}
@Override
public void scroll(int scrollTop, int scrollLeft) {
UI.this.scrollTop = scrollTop;
UI.this.scrollLeft = scrollLeft;
}
@Override
public void poll() {
fireEvent(new PollEvent(UI.this));
}
@Override
public void popstate(String uri) {
getPage().updateLocation(uri, true, true);
}
};
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
@Override
public void showServerDebugInfo(Connector connector) {
String info = ConnectorHelper
.getDebugInformation((ClientConnector) connector);
getLogger().info(info);
}
@Override
public void analyzeLayouts() {
// TODO Move to client side
List invalidSizes = ComponentSizeValidator
.validateLayouts(UI.this);
StringBuilder json = new StringBuilder();
json.append("{\"invalidLayouts\":");
json.append('[');
if (invalidSizes != null) {
boolean first = true;
for (InvalidLayout invalidSize : invalidSizes) {
if (!first) {
json.append(',');
} else {
first = false;
}
invalidSize.reportErrors(json, System.err);
}
}
json.append("]}");
getRpcProxy(DebugWindowClientRpc.class)
.reportLayoutProblems(json.toString());
}
@Override
public void showServerDesign(Connector connector) {
if (!(connector instanceof Component)) {
getLogger().severe("Tried to output declarative design for "
+ connector + ", which is not a component");
return;
}
if (connector instanceof UI) {
// We want to see the content of the UI, so we can add it to
// another UI or component container
connector = ((UI) connector).getContent();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
Design.write((Component) connector, baos);
getLogger().info("Design for " + connector
+ " requested from debug window:\n"
+ baos.toString(UTF_8.name()));
} catch (IOException e) {
getLogger().log(Level.WARNING,
"Error producing design for " + connector, e);
}
}
};
private WindowOrderRpc windowOrderRpc = windowOrders -> {
Map orders = new LinkedHashMap<>();
for (Entry entry : windowOrders.entrySet()) {
if (entry.getValue() instanceof Window) {
orders.put(entry.getKey(), (Window) entry.getValue());
}
}
fireWindowOrder(orders);
};
/**
* Timestamp keeping track of the last heartbeat of this UI. Updated to the
* current time whenever the application receives a heartbeat or UIDL
* request from the client for this UI.
*/
private long lastHeartbeatTimestamp = System.currentTimeMillis();
private boolean closing = false;
private TooltipConfiguration tooltipConfiguration = new TooltipConfigurationImpl(
this);
private PushConfiguration pushConfiguration = new PushConfigurationImpl(
this);
private ReconnectDialogConfiguration reconnectDialogConfiguration = new ReconnectDialogConfigurationImpl(
this);
private NotificationConfiguration notificationConfiguration = new NotificationConfigurationImpl(
this);
/**
* Tracks which message from the client should come next. First message from
* the client has id 0.
*/
private int lastProcessedClientToServerId = -1;
/**
* Stores the extension of the active drag source component
*/
private DragSourceExtension extends AbstractComponent> activeDragSource;
// Despite being static final, a counter is not a constant.
// CHECKSTYLE:OFF
// Counter of DelayedCallbackRegistrations during application life-cycle.
private static final AtomicLong delayedCallbackCounter = new AtomicLong(0);
// CHECKSTYLE:ON
private final Set pendingDelayedCallbacks = new LinkedHashSet<>();
private DelayedCallbackRpc delayedCallbackRpc = received -> {
List toTrigger = new ArrayList<>();
for (InternalDelayedCallbackRegistration callback : pendingDelayedCallbacks) {
Long id = callback.getId();
if (id <= received) {
toTrigger.add(callback);
} else {
// LinkedHashSet maintains insertion order, registration IDs
// increment, and registrations are always inserted
// immediately, no need to go through the whole set
break;
}
}
toTrigger.forEach(registration -> registration.increment());
};
/**
* Creates a new empty UI without a caption. The content of the UI must be
* set by calling {@link #setContent(Component)} before using the UI.
*/
public UI() {
this(null);
}
/**
* Creates a new UI with the given component (often a layout) as its
* content.
*
* @param content
* the component to use as this UIs content.
*
* @see #setContent(Component)
*/
public UI(Component content) {
registerRpc(rpc);
registerRpc(debugRpc);
registerRpc(windowOrderRpc);
registerRpc(delayedCallbackRpc);
setSizeFull();
setContent(content);
}
@Override
protected UIState getState() {
return (UIState) super.getState();
}
@Override
protected UIState getState(boolean markAsDirty) {
return (UIState) super.getState(markAsDirty);
}
@Override
public Class extends UIState> getStateType() {
// This is a workaround for a problem with creating the correct state
// object during build
return UIState.class;
}
/**
* Overridden to return a value instead of referring to the parent.
*
* @return this UI
*
* @see com.vaadin.ui.AbstractComponent#getUI()
*/
@Override
public UI getUI() {
return this;
}
/**
* Gets the application object to which the component is attached.
*
*
* The method will return {@code null} if the component is not currently
* attached to an application.
*
*
*
* Getting a null value is often a problem in constructors of regular
* components and in the initializers of custom composite components. A
* standard workaround is to use {@link VaadinSession#getCurrent()} to
* retrieve the application instance that the current request relates to.
* Another way is to move the problematic initialization to
* {@link #attach()}, as described in the documentation of the method.
*
*
* @return the parent application of the component or null
.
* @see #attach()
*/
@Override
public VaadinSession getSession() {
return session;
}
@Override
public void paintContent(PaintTarget target) throws PaintException {
page.paintContent(target);
if (scrollIntoView != null) {
target.addAttribute("scrollTo", scrollIntoView);
scrollIntoView = null;
}
if (pendingFocus != null) {
// ensure focused component is still attached to this main window
if (equals(pendingFocus.getUI()) || (pendingFocus.getUI() != null
&& equals(pendingFocus.getUI().getParent()))) {
target.addAttribute("focused", pendingFocus);
}
pendingFocus = null;
}
if (actionManager != null) {
actionManager.paintActions(null, target);
}
if (isResizeLazy()) {
target.addAttribute(UIConstants.RESIZE_LAZY, true);
}
}
/**
* Fire a click event to all click listeners.
*
* @param object
* The raw "value" of the variable change from the client side.
*/
private void fireClick(Map parameters) {
MouseEventDetails mouseDetails = MouseEventDetails
.deSerialize((String) parameters.get("mouseDetails"));
fireEvent(new ClickEvent(this, mouseDetails));
}
/**
* Fire a window order event.
*
* @param windows
* The windows with their orders whose order has been updated.
*/
private void fireWindowOrder(Map windows) {
for (Entry entry : windows.entrySet()) {
entry.getValue().fireWindowOrderChange(entry.getKey());
}
fireEvent(new WindowOrderUpdateEvent(this, windows.values()));
}
@Override
@SuppressWarnings("unchecked")
public void changeVariables(Object source, Map variables) {
if (variables.containsKey(EventId.CLICK_EVENT_IDENTIFIER)) {
fireClick((Map) variables
.get(EventId.CLICK_EVENT_IDENTIFIER));
}
// Actions
if (actionManager != null) {
actionManager.handleActions(variables, this);
}
}
/*
* (non-Javadoc)
*
* @see com.vaadin.ui.HasComponents#iterator()
*/
@Override
public Iterator iterator() {
// TODO could directly create some kind of combined iterator instead of
// creating a new ArrayList
List components = new ArrayList<>();
if (getContent() != null) {
components.add(getContent());
}
components.addAll(windows);
return Collections.unmodifiableCollection(components).iterator();
}
/*
* (non-Javadoc)
*
* @see com.vaadin.ui.ComponentContainer#getComponentCount()
*/
@Override
public int getComponentCount() {
return windows.size() + (getContent() == null ? 0 : 1);
}
/**
* Sets the session to which this UI is assigned.
*
* This method is for internal use by the framework. To explicitly close a
* UI, see {@link #close()}.
*
*
* @param session
* the session to set
*
* @throws IllegalStateException
* if the session has already been set
*
* @see #getSession()
*/
public void setSession(VaadinSession session) {
if (session == null && this.session == null) {
throw new IllegalStateException(
"Session should never be set to null when UI.session is already null");
} else if (session != null && this.session != null) {
throw new IllegalStateException(
"Session has already been set. Old session: "
+ getSessionDetails(this.session)
+ ". New session: " + getSessionDetails(session)
+ ".");
} else {
if (session == null) {
try {
detach();
} catch (Exception e) {
getLogger().log(Level.WARNING,
"Error while detaching UI from session", e);
}
// Disable push when the UI is detached. Otherwise the
// push connection and possibly VaadinSession will live
// on.
getPushConfiguration().setPushMode(PushMode.DISABLED);
new Thread(() -> {
// This intentionally does disconnect without locking
// the VaadinSession to avoid deadlocks where the server
// uses a lock for the websocket connection
// See https://dev.vaadin.com/ticket/18436
// The underlying problem is
// https://dev.vaadin.com/ticket/16919
setPushConnection(null);
}).start();
}
this.session = session;
}
if (session != null) {
attach();
}
}
private static String getSessionDetails(VaadinSession session) {
if (session == null) {
return null;
} else {
return session + " for " + session.getService().getServiceName();
}
}
/**
* Gets the id of the UI, used to identify this UI within its application
* when processing requests. The UI id should be present in every request to
* the server that originates from this UI.
* {@link VaadinService#findUI(VaadinRequest)} uses this id to find the
* route to which the request belongs.
*
* This method is not intended to be overridden. If it is overridden, care
* should be taken since this method might be called in situations where
* {@link UI#getCurrent()} does not return this UI.
*
* @return the id of this UI
*/
public int getUIId() {
return uiId;
}
/**
* Adds a window as a subwindow inside this UI. To open a new browser window
* or tab, you should instead use a {@link UIProvider}.
*
* @param window
* @throws IllegalArgumentException
* if the window is already added to an application
* @throws NullPointerException
* if the given Window
is null
.
*/
public void addWindow(Window window)
throws IllegalArgumentException, NullPointerException {
if (window == null) {
throw new NullPointerException("Argument must not be null");
}
if (window.isAttached()) {
throw new IllegalArgumentException(
"Window is already attached to an application.");
}
attachWindow(window);
}
/**
* Helper method to attach a window.
*
* @param w
* the window to add
*/
private void attachWindow(Window w) {
windows.add(w);
w.setParent(this);
fireComponentAttachEvent(w);
markAsDirty();
}
/**
* Remove the given subwindow from this UI.
*
* Since Vaadin 6.5, {@link Window.CloseListener}s are called also when
* explicitly removing a window by calling this method.
*
* Since Vaadin 6.5, returns a boolean indicating if the window was removed
* or not.
*
* @param window
* Window to be removed.
* @return true if the subwindow was removed, false otherwise
*/
public boolean removeWindow(Window window) {
if (!windows.remove(window)) {
// Window window is not a subwindow of this UI.
return false;
}
window.setParent(null);
markAsDirty();
window.fireClose();
fireComponentDetachEvent(window);
fireWindowOrder(Collections.singletonMap(-1, window));
return true;
}
/**
* Gets all the windows added to this UI.
*
* @return an unmodifiable collection of windows
*/
public Collection getWindows() {
return Collections.unmodifiableCollection(windows);
}
@Override
public void focus() {
super.focus();
}
/**
* Component that should be focused after the next repaint. Null if no focus
* change should take place.
*/
private Focusable pendingFocus;
private boolean resizeLazy = false;
private Navigator navigator;
private PushConnection pushConnection = null;
private LocaleService localeService = new LocaleService(this,
getState(false).localeServiceState);
private String embedId;
private String uiPathInfo;
private String uiRootPath;
private boolean mobileHtml5DndPolyfillLoaded;
/**
* This method is used by Component.Focusable objects to request focus to
* themselves. Focus renders must be handled at window level (instead of
* Component.Focusable) due we want the last focused component to be focused
* in client too. Not the one that is rendered last (the case we'd get if
* implemented in Focusable only).
*
* To focus component from Vaadin application, use Focusable.focus(). See
* {@link Focusable}.
*
* @param focusable
* to be focused on next paint
*/
public void setFocusedComponent(Focusable focusable) {
pendingFocus = focusable;
markAsDirty();
}
/**
* Scrolls any component between the component and UI to a suitable position
* so the component is visible to the user. The given component must belong
* to this UI.
*
* @param component
* the component to be scrolled into view
* @throws IllegalArgumentException
* if {@code component} does not belong to this UI
*/
public void scrollIntoView(Component component)
throws IllegalArgumentException {
if (component.getUI() != this) {
throw new IllegalArgumentException(
"The component where to scroll must belong to this UI.");
}
scrollIntoView = component;
markAsDirty();
}
/**
* Internal initialization method, should not be overridden. This method is
* not declared as final because that would break compatibility with e.g.
* CDI.
*
* @param request
* the initialization request
* @param uiId
* the id of the new ui
* @param embedId
* the embed id of this UI, or null
if no id is
* known
*
* @see #getUIId()
* @see #getEmbedId()
*/
public void doInit(VaadinRequest request, int uiId, String embedId) {
if (this.uiId != -1) {
String message = "This UI instance is already initialized (as UI id "
+ this.uiId
+ ") and can therefore not be initialized again (as UI id "
+ uiId + "). ";
if (getSession() != null
&& !getSession().equals(VaadinSession.getCurrent())) {
message += "Furthermore, it is already attached to another VaadinSession. ";
}
message += "Please make sure you are not accidentally reusing an old UI instance.";
throw new IllegalStateException(message);
}
this.uiId = uiId;
this.embedId = embedId;
// Actual theme - used for finding CustomLayout templates
setTheme(request.getParameter("theme"));
getPage().init(request);
String uiPathInfo = (String) request
.getAttribute(ApplicationConstants.UI_ROOT_PATH);
if (uiPathInfo != null) {
setUiPathInfo(uiPathInfo);
}
if (getSession() != null && getSession().getConfiguration() != null
&& getSession().getConfiguration().isSendUrlsAsParameters()
&& getPage().getLocation() != null) {
// By default the root is the URL from client
String uiRootPath = getPage().getLocation().getPath();
if (uiPathInfo != null && uiRootPath.contains(uiPathInfo)) {
// String everything from the URL after uiPathInfo
// This will remove the navigation state from the URL
uiRootPath = uiRootPath.substring(0,
uiRootPath.indexOf(uiPathInfo) + uiPathInfo.length());
} else if (request.getPathInfo() != null) {
// uiRootPath does not match the uiPathInfo
// This can happen for example when embedding a Vaadin UI
String pathInfo = request.getPathInfo();
if (uiRootPath.endsWith(pathInfo)) {
uiRootPath = uiRootPath.substring(0,
uiRootPath.length() - pathInfo.length());
}
}
// Store the URL as the UI Root Path
setUiRootPath(uiRootPath);
}
// Call the init overridden by the application developer
init(request);
Navigator navigator = getNavigator();
if (navigator != null) {
// Kickstart navigation if a navigator was attached in init()
navigator.navigateTo(navigator.getState());
}
}
private void setUiRootPath(String uiRootPath) {
this.uiRootPath = uiRootPath;
}
/**
* Gets the part of path (from browser's URL) that points to this UI.
* Basically the same as the value from {@link Page#getLocation()}, but
* without possible view identifiers or path parameters.
*
* @return the part of path (from browser's URL) that points to this UI,
* without possible view identifiers or path parameters
*
* @since 8.2
*/
public String getUiRootPath() {
return uiRootPath;
}
private void setUiPathInfo(String uiPathInfo) {
this.uiPathInfo = uiPathInfo;
}
/**
* Gets the path info part of the request that is used to detect the UI.
* This is defined during UI init by certain {@link UIProvider UIProviders}
* that map different UIs to different URIs, like Vaadin Spring. This
* information is used by the {@link Navigator} when the {@link UI} is
* annotated with {@link PushStateNavigation}.
*
* For example if the UI is accessed through
* {@code http://example.com/MyUI/mainview/parameter=1} the path info would
* be {@code /MyUI}.
*
* @return the path info part of the request; {@code null} if no request
* from client has been processed
*
* @since 8.2
*/
public String getUiPathInfo() {
return uiPathInfo;
}
/**
* Initializes this UI. This method is intended to be overridden by
* subclasses to build the view and configure non-component functionality.
* Performing the initialization in a constructor is not suggested as the
* state of the UI is not properly set up when the constructor is invoked.
*
* The {@link VaadinRequest} can be used to get information about the
* request that caused this UI to be created.
*
*
* @param request
* the Vaadin request that caused this UI to be created
*/
protected abstract void init(VaadinRequest request);
/**
* Internal reinitialization method, should not be overridden.
*
* @since 7.2
* @param request
* the request that caused this UI to be reloaded
*/
public void doRefresh(VaadinRequest request) {
// This is a horrible hack. We want to have the most recent location and
// browser window size available in refresh(), but we want to call
// listeners, if any, only after refresh(). So we momentarily assign the
// old values back before setting the new values again to ensure the
// events are properly fired.
PushConnection pushConnection = getPushConnection();
if (pushConnection instanceof AtmospherePushConnection
&& pushConnection.isConnected()) {
pushConnection.disconnect();
}
Page page = getPage();
URI oldLocation = page.getLocation();
int oldWidth = page.getBrowserWindowWidth();
int oldHeight = page.getBrowserWindowHeight();
page.init(request);
// Reset heartbeat timeout to avoid surprise if it's almost expired
setLastHeartbeatTimestamp(System.currentTimeMillis());
refresh(request);
URI newLocation = page.getLocation();
int newWidth = page.getBrowserWindowWidth();
int newHeight = page.getBrowserWindowHeight();
page.updateLocation(oldLocation.toString(), false, false);
page.updateBrowserWindowSize(oldWidth, oldHeight, false);
page.updateLocation(newLocation.toString(), true, false);
page.updateBrowserWindowSize(newWidth, newHeight, true);
// Navigate if there is navigator, this is needed in case of
// PushStateNavigation. Call navigateTo only if state have
// truly changed
Navigator navigator = getNavigator();
if (navigator != null
&& !Objects.equals(navigator.getCurrentNavigationState(),
navigator.getState())) {
navigator.navigateTo(navigator.getState());
}
}
/**
* Reinitializes this UI after a browser refresh if the UI is set to be
* preserved on refresh, typically using the {@link PreserveOnRefresh}
* annotation. This method is intended to be overridden by subclasses if
* needed; the default implementation is empty.
*
* The {@link VaadinRequest} can be used to get information about the
* request that caused this UI to be reloaded.
*
* @since 7.2
* @param request
* the request that caused this UI to be reloaded
*/
protected void refresh(VaadinRequest request) {
}
/**
* Sets the thread local for the current UI. This method is used by the
* framework to set the current application whenever a new request is
* processed and it is cleared when the request has been processed.
*
* The application developer can also use this method to define the current
* UI outside the normal request handling, e.g. when initiating custom
* background threads.
*
* The UI is stored using a weak reference to avoid leaking memory in case
* it is not explicitly cleared.
*
* @param ui
* the UI to register as the current UI
*
* @see #getCurrent()
* @see ThreadLocal
*/
public static void setCurrent(UI ui) {
CurrentInstance.set(UI.class, ui);
}
/**
* Gets the currently used UI. The current UI is automatically defined when
* processing requests to the server. In other cases, (e.g. from background
* threads), the current UI is not automatically defined.
*
* The UI is stored using a weak reference to avoid leaking memory in case
* it is not explicitly cleared.
*
* @return the current UI instance if available, otherwise null
*
* @see #setCurrent(UI)
*/
public static UI getCurrent() {
return CurrentInstance.get(UI.class);
}
/**
* Set top offset to which the UI should scroll to.
*
* @param scrollTop
*/
public void setScrollTop(int scrollTop) {
if (scrollTop < 0) {
throw new IllegalArgumentException(
"Scroll offset must be at least 0");
}
if (this.scrollTop != scrollTop) {
this.scrollTop = scrollTop;
getRpcProxy(ScrollClientRpc.class).setScrollTop(scrollTop);
}
}
public int getScrollTop() {
return scrollTop;
}
/**
* Set left offset to which the UI should scroll to.
*
* @param scrollLeft
*/
public void setScrollLeft(int scrollLeft) {
if (scrollLeft < 0) {
throw new IllegalArgumentException(
"Scroll offset must be at least 0");
}
if (this.scrollLeft != scrollLeft) {
this.scrollLeft = scrollLeft;
getRpcProxy(ScrollClientRpc.class).setScrollLeft(scrollLeft);
}
}
public int getScrollLeft() {
return scrollLeft;
}
@Override
protected ActionManager getActionManager() {
if (actionManager == null) {
actionManager = new ActionManager(this);
}
return actionManager;
}
@Override
public void addAction(
T action) {
getActionManager().addAction(action);
}
@Override
public void removeAction(
T action) {
if (actionManager != null) {
actionManager.removeAction(action);
}
}
@Override
public void addActionHandler(Handler actionHandler) {
getActionManager().addActionHandler(actionHandler);
}
@Override
public void removeActionHandler(Handler actionHandler) {
if (actionManager != null) {
actionManager.removeActionHandler(actionHandler);
}
}
/**
* Should resize operations be lazy, i.e. should there be a delay before
* layout sizes are recalculated and resize events are sent to the server.
* Speeds up resize operations in slow UIs with the penalty of slightly
* decreased usability.
*
* Default value: false
*
*
* When there are active window resize listeners, lazy resize mode should be
* used to avoid a large number of events during resize.
*
*
* @param resizeLazy
* true to use a delay before recalculating sizes, false to
* calculate immediately.
*/
public void setResizeLazy(boolean resizeLazy) {
this.resizeLazy = resizeLazy;
markAsDirty();
}
/**
* Checks whether lazy resize is enabled.
*
* @return true
if lazy resize is enabled, false
* if lazy resize is not enabled
*/
public boolean isResizeLazy() {
return resizeLazy;
}
/**
* Add a click listener to the UI. The listener is called whenever the user
* clicks inside the UI. Also when the click targets a component inside the
* UI, provided the targeted component does not prevent the click event from
* propagating.
*
* @see Registration
*
* @param listener
* The listener to add, not null
* @return a registration object for removing the listener
* @since 8.0
*/
public Registration addClickListener(ClickListener listener) {
return addListener(EventId.CLICK_EVENT_IDENTIFIER, ClickEvent.class,
listener, ClickListener.clickMethod);
}
/**
* Remove a click listener from the UI. The listener should earlier have
* been added using {@link #addClickListener(ClickListener)}.
*
* @param listener
* The listener to remove
*
* @deprecated As of 8.0, replaced by {@link Registration#remove()} in the
* registration object returned from
* {@link #removeClickListener(ClickListener)}.
*/
@Deprecated
public void removeClickListener(ClickListener listener) {
removeListener(EventId.CLICK_EVENT_IDENTIFIER, ClickEvent.class,
listener);
}
@Override
public boolean isConnectorEnabled() {
// TODO How can a UI be invisible? What does it mean?
return isVisible() && isEnabled();
}
public ConnectorTracker getConnectorTracker() {
return connectorTracker;
}
public Page getPage() {
return page;
}
/**
* Returns the navigator attached to this UI or null if there is no
* navigator.
*
* @return
*/
public Navigator getNavigator() {
return navigator;
}
/**
* For internal use only.
*
* @param navigator
*/
public void setNavigator(Navigator navigator) {
this.navigator = navigator;
}
/**
* Setting the caption of a UI is not supported. To set the title of the
* HTML page, use Page.setTitle
*
* @deprecated As of 7.0, use {@link Page#setTitle(String)}
*/
@Override
@Deprecated
public void setCaption(String caption) {
throw new UnsupportedOperationException(
"You can not set the title of a UI. To set the title of the HTML page, use Page.setTitle");
}
/**
* Shows a notification message on the middle of the UI. The message
* automatically disappears ("humanized message").
*
* Care should be taken to to avoid XSS vulnerabilities as the caption is
* rendered as html.
*
* @see #showNotification(Notification)
* @see Notification
*
* @param caption
* The message
*
* @deprecated As of 7.0, use Notification.show instead but be aware that
* Notification.show does not allow HTML.
*/
@Deprecated
public void showNotification(String caption) {
Notification notification = new Notification(caption);
notification.setHtmlContentAllowed(true);// Backwards compatibility
getPage().showNotification(notification);
}
/**
* Shows a notification message the UI. The position and behavior of the
* message depends on the type, which is one of the basic types defined in
* {@link Notification}, for instance Notification.TYPE_WARNING_MESSAGE.
*
* Care should be taken to to avoid XSS vulnerabilities as the caption is
* rendered as html.
*
* @see #showNotification(Notification)
* @see Notification
*
* @param caption
* The message
* @param type
* The message type
*
* @deprecated As of 7.0, use Notification.show instead but be aware that
* Notification.show does not allow HTML.
*/
@Deprecated
public void showNotification(String caption, Notification.Type type) {
Notification notification = new Notification(caption, type);
notification.setHtmlContentAllowed(true);// Backwards compatibility
getPage().showNotification(notification);
}
/**
* Shows a notification consisting of a bigger caption and a smaller
* description on the middle of the UI. The message automatically disappears
* ("humanized message").
*
* Care should be taken to to avoid XSS vulnerabilities as the caption and
* description are rendered as html.
*
* @see #showNotification(Notification)
* @see Notification
*
* @param caption
* The caption of the message
* @param description
* The message description
*
* @deprecated As of 7.0, use new Notification(...).show(Page) instead but
* be aware that HTML by default not allowed.
*/
@Deprecated
public void showNotification(String caption, String description) {
Notification notification = new Notification(caption, description);
notification.setHtmlContentAllowed(true);// Backwards compatibility
getPage().showNotification(notification);
}
/**
* Shows a notification consisting of a bigger caption and a smaller
* description. The position and behavior of the message depends on the
* type, which is one of the basic types defined in {@link Notification} ,
* for instance Notification.TYPE_WARNING_MESSAGE.
*
* Care should be taken to to avoid XSS vulnerabilities as the caption and
* description are rendered as html.
*
* @see #showNotification(Notification)
* @see Notification
*
* @param caption
* The caption of the message
* @param description
* The message description
* @param type
* The message type
*
* @deprecated As of 7.0, use new Notification(...).show(Page) instead but
* be aware that HTML by default not allowed.
*/
@Deprecated
public void showNotification(String caption, String description,
Notification.Type type) {
Notification notification = new Notification(caption, description,
type);
notification.setHtmlContentAllowed(true);// Backwards compatibility
getPage().showNotification(notification);
}
/**
* Shows a notification consisting of a bigger caption and a smaller
* description. The position and behavior of the message depends on the
* type, which is one of the basic types defined in {@link Notification} ,
* for instance Notification.TYPE_WARNING_MESSAGE.
*
* Care should be taken to avoid XSS vulnerabilities if html content is
* allowed.
*
* @see #showNotification(Notification)
* @see Notification
*
* @param caption
* The message caption
* @param description
* The message description
* @param type
* The type of message
* @param htmlContentAllowed
* Whether html in the caption and description should be
* displayed as html or as plain text
*
* @deprecated As of 7.0, use new Notification(...).show(Page).
*/
@Deprecated
public void showNotification(String caption, String description,
Notification.Type type, boolean htmlContentAllowed) {
getPage().showNotification(new Notification(caption, description, type,
htmlContentAllowed));
}
/**
* Shows a notification message.
*
* @see Notification
* @see #showNotification(String)
* @see #showNotification(String, String)
*
* @param notification
* The notification message to show
*
* @deprecated As of 7.0, use Notification.show instead
*/
@Deprecated
public void showNotification(Notification notification) {
getPage().showNotification(notification);
}
/**
* Returns the timestamp of the last received heartbeat for this UI.
*
* This method is not intended to be overridden. If it is overridden, care
* should be taken since this method might be called in situations where
* {@link UI#getCurrent()} does not return this UI.
*
* @see VaadinService#closeInactiveUIs(VaadinSession)
*
* @return The time the last heartbeat request occurred, in milliseconds
* since the epoch.
*/
public long getLastHeartbeatTimestamp() {
return lastHeartbeatTimestamp;
}
/**
* Sets the last heartbeat request timestamp for this UI. Called by the
* framework whenever the application receives a valid heartbeat request for
* this UI.
*
* This method is not intended to be overridden. If it is overridden, care
* should be taken since this method might be called in situations where
* {@link UI#getCurrent()} does not return this UI.
*
* @param lastHeartbeat
* The time the last heartbeat request occurred, in milliseconds
* since the epoch.
*/
public void setLastHeartbeatTimestamp(long lastHeartbeat) {
lastHeartbeatTimestamp = lastHeartbeat;
}
/**
* Gets the theme currently in use by this UI.
*
* @return the theme name
*/
public String getTheme() {
return getState(false).theme;
}
/**
* Sets the theme currently in use by this UI
*
* Calling this method will remove the old theme (CSS file) from the
* application and add the new theme.
*
* Note that this method is NOT SAFE to call in a portal environment or
* other environment where there are multiple UIs on the same page. The old
* CSS file will be removed even if there are other UIs on the page which
* are still using it.
*
* @since 7.3
* @param theme
* The new theme name
*/
public void setTheme(String theme) {
if (theme == null) {
getState().theme = null;
} else {
getState().theme = VaadinServlet.stripSpecialChars(theme);
}
}
/**
* Marks this UI to be {@link #detach() detached} from the session at the
* end of the current request, or the next request if there is no current
* request (if called from a background thread, for instance.)
*
* The UI is detached after the response is sent, so in the current request
* it can still update the client side normally. However, after the response
* any new requests from the client side to this UI will cause an error, so
* usually the client should be asked, for instance, to reload the page
* (serving a fresh UI instance), to close the page, or to navigate
* somewhere else.
*
* Note that this method is strictly for users to explicitly signal the
* framework that the UI should be detached. Overriding it is not a reliable
* way to catch UIs that are to be detached. Instead, {@code UI.detach()}
* should be overridden or a {@link DetachListener} used.
*/
public void close() {
closing = true;
boolean sessionExpired = (session == null
|| session.getState() != State.OPEN);
getRpcProxy(UIClientRpc.class).uiClosed(sessionExpired);
if (getPushConnection() != null) {
// Push the Rpc to the client. The connection will be closed when
// the UI is detached and cleaned up.
// Can't use UI.push() directly since it checks for a valid session
if (session != null) {
session.getService().runPendingAccessTasks(session);
}
getPushConnection().push();
}
}
/**
* Returns whether this UI is marked as closed and is to be detached.
*
* This method is not intended to be overridden. If it is overridden, care
* should be taken since this method might be called in situations where
* {@link UI#getCurrent()} does not return this UI.
*
* @see #close()
*
* @return whether this UI is closing.
*/
public boolean isClosing() {
return closing;
}
/**
* Called after the UI is added to the session. A UI instance is attached
* exactly once, before its {@link #init(VaadinRequest) init} method is
* called.
*
* @see Component#attach
*/
@Override
public void attach() {
super.attach();
getLocaleService().addLocale(getLocale());
}
/**
* Called before the UI is removed from the session. A UI instance is
* detached exactly once, either:
*
* - after it is explicitly {@link #close() closed}.
*
- when its session is closed or expires
*
- after three missed heartbeat requests.
*
*
* Note that when a UI is detached, any changes made in the {@code detach}
* methods of any children or {@link DetachListener}s that would be
* communicated to the client are silently ignored.
*/
@Override
public void detach() {
if (!pendingDelayedCallbacks.isEmpty()) {
getLogger().info(
"UI detached before all pending callbacks could be processed. "
+ "Cancelling the remaining callbacks.");
// each call to #cancel() modifies the set, iterate over a copy
for (DelayedCallbackRegistration registration : new LinkedHashSet<>(
pendingDelayedCallbacks)) {
registration.cancel();
}
// should be empty already but just in case
pendingDelayedCallbacks.clear();
}
super.detach();
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.ui.AbstractSingleComponentContainer#setContent(com.vaadin.
* ui.Component)
*/
@Override
public void setContent(Component content) {
if (content instanceof Window) {
throw new IllegalArgumentException(
"A Window cannot be added using setContent. Use addWindow(Window window) instead");
}
super.setContent(content);
}
@Override
public void setTabIndex(int tabIndex) {
getState().tabIndex = tabIndex;
}
@Override
public int getTabIndex() {
return getState(false).tabIndex;
}
/**
* Locks the session of this UI and runs the provided Runnable right away.
*
* It is generally recommended to use {@link #access(Runnable)} instead of
* this method for accessing a session from a different thread as
* {@link #access(Runnable)} can be used while holding the lock of another
* session. To avoid causing deadlocks, this methods throws an exception if
* it is detected than another session is also locked by the current thread.
*
* This method behaves differently than {@link #access(Runnable)} in some
* situations:
*
* - If the current thread is currently holding the lock of the session,
* {@link #accessSynchronously(Runnable)} runs the task right away whereas
* {@link #access(Runnable)} defers the task to a later point in time.
* - If some other thread is currently holding the lock for the session,
* {@link #accessSynchronously(Runnable)} blocks while waiting for the lock
* to be available whereas {@link #access(Runnable)} defers the task to a
* later point in time.
*
*
* @since 7.1
*
* @param runnable
* the runnable which accesses the UI
* @throws UIDetachedException
* if the UI is not attached to a session (and locking can
* therefore not be done)
* @throws IllegalStateException
* if the current thread holds the lock for another session
*
* @see #access(Runnable)
* @see VaadinSession#accessSynchronously(Runnable)
*/
public void accessSynchronously(Runnable runnable)
throws UIDetachedException {
Map, CurrentInstance> old = null;
VaadinSession session = getSession();
if (session == null) {
throw new UIDetachedException();
}
VaadinService.verifyNoOtherSessionLocked(session);
session.lock();
try {
if (getSession() == null) {
// UI was detached after fetching the session but before we
// acquired the lock.
throw new UIDetachedException();
}
old = CurrentInstance.setCurrent(this);
runnable.run();
} finally {
session.unlock();
if (old != null) {
CurrentInstance.restoreInstances(old);
}
}
}
/**
* Provides exclusive access to this UI from outside a request handling
* thread.
*
* The given runnable is executed while holding the session lock to ensure
* exclusive access to this UI. If the session is not locked, the lock will
* be acquired and the runnable is run right away. If the session is
* currently locked, the runnable will be run before that lock is released.
*
*
* RPC handlers for components inside this UI do not need to use this method
* as the session is automatically locked by the framework during RPC
* handling.
*
*
* Please note that the runnable might be invoked on a different thread or
* later on the current thread, which means that custom thread locals might
* not have the expected values when the command is executed.
* {@link UI#getCurrent()}, {@link VaadinSession#getCurrent()} and
* {@link VaadinService#getCurrent()} are set according to this UI before
* executing the command. Other standard CurrentInstance values such as
* {@link VaadinService#getCurrentRequest()} and
* {@link VaadinService#getCurrentResponse()} will not be defined.
*
*
* The returned future can be used to check for task completion and to
* cancel the task.
*
*
* @see #getCurrent()
* @see #accessSynchronously(Runnable)
* @see VaadinSession#access(Runnable)
* @see VaadinSession#lock()
*
* @since 7.1
*
* @param runnable
* the runnable which accesses the UI
* @throws UIDetachedException
* if the UI is not attached to a session (and locking can
* therefore not be done)
* @return a future that can be used to check for task completion and to
* cancel the task
*/
public Future access(final Runnable runnable) {
VaadinSession session = getSession();
if (session == null) {
throw new UIDetachedException();
}
return session.access(new ErrorHandlingRunnable() {
@Override
public void run() {
accessSynchronously(runnable);
}
@Override
public void handleError(Exception exception) {
try {
exception = ErrorHandlingRunnable.processException(runnable,
exception);
if (exception instanceof UIDetachedException) {
assert session != null;
/*
* UI was detached after access was run, but before
* accessSynchronously. Furthermore, there wasn't an
* ErrorHandlingRunnable that handled the exception.
*/
getLogger().log(Level.WARNING, "access() task ignored "
+ "because UI got detached after the task was "
+ "enqueued. To suppress this message, change "
+ "the task to implement {0} and make it handle "
+ "{1}. Affected task: {2}",
new Object[] {
ErrorHandlingRunnable.class.getName(),
UIDetachedException.class.getName(),
runnable });
} else if (exception != null) {
/*
* If no ErrorHandlingRunnable, or if it threw an
* exception of its own.
*/
ConnectorErrorEvent errorEvent = new ConnectorErrorEvent(
UI.this, exception);
ErrorHandler errorHandler = com.vaadin.server.ErrorEvent
.findErrorHandler(UI.this);
if (errorHandler == null && getSession() == null) {
/*
* Special case where findErrorHandler(UI) cannot
* find the session handler because the UI has
* recently been detached.
*/
errorHandler = com.vaadin.server.ErrorEvent
.findErrorHandler(session);
}
if (errorHandler == null) {
errorHandler = new DefaultErrorHandler();
}
errorHandler.error(errorEvent);
}
} catch (Exception e) {
getLogger().log(Level.SEVERE, e.getMessage(), e);
}
}
});
}
/**
* Retrieves the object used for configuring tooltips.
*
* @return The instance used for tooltip configuration
*/
public TooltipConfiguration getTooltipConfiguration() {
return tooltipConfiguration;
}
/**
* Retrieves the object used for configuring notifications.
*
* @return The instance used for notification configuration
*/
public NotificationConfiguration getNotificationConfiguration() {
return notificationConfiguration;
}
/**
* Retrieves the object used for configuring the loading indicator.
*
* @return The instance used for configuring the loading indicator
*/
public LoadingIndicatorConfiguration getLoadingIndicatorConfiguration() {
return loadingIndicatorConfiguration;
}
/**
* Pushes the pending changes and client RPC invocations of this UI to the
* client-side.
*
* If push is enabled, but the push connection is not currently open, the
* push will be done when the connection is established.
*
* As with all UI methods, the session must be locked when calling this
* method. It is also recommended that {@link UI#getCurrent()} is set up to
* return this UI since writing the response may invoke logic in any
* attached component or extension. The recommended way of fulfilling these
* conditions is to use {@link #access(Runnable)}.
*
* @throws IllegalStateException
* if push is disabled.
* @throws UIDetachedException
* if this UI is not attached to a session.
*
* @see #getPushConfiguration()
*
* @since 7.1
*/
public void push() {
VaadinSession session = getSession();
if (session == null) {
throw new UIDetachedException("Cannot push a detached UI");
}
assert session.hasLock();
if (!getPushConfiguration().getPushMode().isEnabled()) {
throw new IllegalStateException("Push not enabled");
}
assert pushConnection != null;
/*
* Purge the pending access queue as it might mark a connector as dirty
* when the push would otherwise be ignored because there are no changes
* to push.
*/
session.getService().runPendingAccessTasks(session);
if (!getConnectorTracker().hasDirtyConnectors()) {
// Do not push if there is nothing to push
return;
}
pushConnection.push();
}
/**
* Returns the internal push connection object used by this UI. This method
* should only be called by the framework.
*
* This method is not intended to be overridden. If it is overridden, care
* should be taken since this method might be called in situations where
* {@link UI#getCurrent()} does not return this UI.
*
* @return the push connection used by this UI, or {@code null} if push is
* not available.
*/
public PushConnection getPushConnection() {
assert !(getPushConfiguration().getPushMode().isEnabled()
&& pushConnection == null);
return pushConnection;
}
/**
* Sets the internal push connection object used by this UI. This method
* should only be called by the framework.
*
* The {@code pushConnection} argument must be non-null if and only if
* {@code getPushConfiguration().getPushMode().isEnabled()}.
*
* @param pushConnection
* the push connection to use for this UI
*/
public void setPushConnection(PushConnection pushConnection) {
// If pushMode is disabled then there should never be a pushConnection;
// if enabled there should always be
assert (pushConnection == null)
^ getPushConfiguration().getPushMode().isEnabled();
if (pushConnection == this.pushConnection) {
return;
}
if (this.pushConnection != null && this.pushConnection.isConnected()) {
this.pushConnection.disconnect();
}
this.pushConnection = pushConnection;
}
/**
* Sets the interval with which the UI should poll the server to see if
* there are any changes. Polling is disabled by default.
*
* Note that it is possible to enable push and polling at the same time but
* it should not be done to avoid excessive server traffic.
*
*
* Add-on developers should note that this method is only meant for the
* application developer. An add-on should not set the poll interval
* directly, rather instruct the user to set it.
*
*
* @param intervalInMillis
* The interval (in ms) with which the UI should poll the server
* or -1 to disable polling
*/
public void setPollInterval(int intervalInMillis) {
getState().pollInterval = intervalInMillis;
}
/**
* Returns the interval with which the UI polls the server.
*
* @return The interval (in ms) with which the UI polls the server or -1 if
* polling is disabled
*/
public int getPollInterval() {
return getState(false).pollInterval;
}
@Override
public Registration addPollListener(PollListener listener) {
return addListener(EventId.POLL, PollEvent.class, listener,
PollListener.POLL_METHOD);
}
@Override
@Deprecated
public void removePollListener(PollListener listener) {
removeListener(EventId.POLL, PollEvent.class, listener);
}
/**
* Retrieves the object used for configuring the push channel.
*
* @since 7.1
* @return The instance used for push configuration
*/
public PushConfiguration getPushConfiguration() {
return pushConfiguration;
}
/**
* Retrieves the object used for configuring the reconnect dialog.
*
* @since 7.6
* @return The instance used for reconnect dialog configuration
*/
public ReconnectDialogConfiguration getReconnectDialogConfiguration() {
return reconnectDialogConfiguration;
}
/**
* Get the label that is added to the container element, where tooltip,
* notification and dialogs are added to.
*
* @return the label of the container
*/
public String getOverlayContainerLabel() {
return getState(false).overlayContainerLabel;
}
/**
* Sets the label that is added to the container element, where tooltip,
* notifications and dialogs are added to.
*
* This is helpful for users of assistive devices, as this element is
* reachable for them.
*
*
* @param overlayContainerLabel
* label to use for the container
*/
public void setOverlayContainerLabel(String overlayContainerLabel) {
getState().overlayContainerLabel = overlayContainerLabel;
}
/**
* Returns the locale service which handles transmission of Locale data to
* the client.
*
* @since 7.1
* @return The LocaleService for this UI
*/
public LocaleService getLocaleService() {
return localeService;
}
private static Logger getLogger() {
return Logger.getLogger(UI.class.getName());
}
/**
* Gets a string the uniquely distinguishes this UI instance based on where
* it is embedded. The embed identifier is based on the
* window.name
DOM attribute of the browser window where the UI
* is displayed and the id of the div element where the UI is embedded.
*
* @since 7.2
* @return the embed id for this UI, or null
if no id known
*/
public String getEmbedId() {
return embedId;
}
/**
* Gets the last processed server message id.
*
* Used internally for communication tracking.
*
* @return lastProcessedServerMessageId the id of the last processed server
* message
* @since 7.6
*/
public int getLastProcessedClientToServerId() {
return lastProcessedClientToServerId;
}
/**
* Sets the last processed server message id.
*
* Used internally for communication tracking.
*
* @param lastProcessedClientToServerId
* the id of the last processed server message
* @since 7.6
*/
public void setLastProcessedClientToServerId(
int lastProcessedClientToServerId) {
this.lastProcessedClientToServerId = lastProcessedClientToServerId;
}
/**
* Adds a WindowOrderUpdateListener to the UI.
*
* The WindowOrderUpdateEvent is fired when the order positions of windows
* are updated. It can happen when some window (this or other) is brought to
* front or detached.
*
* The other way to listen window position for specific window is
* {@link Window#addWindowOrderChangeListener(WindowOrderChangeListener)}
*
* @see Window#addWindowOrderChangeListener(WindowOrderChangeListener)
*
* @param listener
* the WindowModeChangeListener to add.
* @since 8.0
*
* @return a registration object for removing the listener
*/
public Registration addWindowOrderUpdateListener(
WindowOrderUpdateListener listener) {
addListener(EventId.WINDOW_ORDER, WindowOrderUpdateEvent.class,
listener, WindowOrderUpdateListener.windowOrderUpdateMethod);
return () -> removeListener(EventId.WINDOW_ORDER,
WindowOrderUpdateEvent.class, listener);
}
/**
* Sets the drag source of an active HTML5 drag event.
*
* @param extension
* Extension of the drag source component.
* @see DragSourceExtension
* @since 8.1
*/
public void setActiveDragSource(
DragSourceExtension extends AbstractComponent> extension) {
activeDragSource = extension;
}
/**
* Gets the drag source of an active HTML5 drag event.
*
* @return Extension of the drag source component if the drag event is
* active and originated from this UI, {@literal null} otherwise.
* @see DragSourceExtension
* @since 8.1
*/
public DragSourceExtension extends AbstractComponent> getActiveDragSource() {
return activeDragSource;
}
/**
* Returns whether HTML5 DnD extensions {@link DragSourceExtension} and
* {@link DropTargetExtension} and alike should be enabled for mobile
* devices.
*
* By default, it is disabled.
*
* @return {@code true} if enabled, {@code false} if not
* @since 8.1
* @see #setMobileHtml5DndEnabled(boolean)
*/
public boolean isMobileHtml5DndEnabled() {
return getState(false).enableMobileHTML5DnD;
}
/**
* Enable or disable HTML5 DnD for mobile devices.
*
* Usually you should enable the support in the {@link #init(VaadinRequest)}
* method. By default, it is disabled. This operation is NOOP when the user
* is not on a mobile device.
*
* Changing this will effect all {@link DragSourceExtension} and
* {@link DropTargetExtension} (and subclasses) that have not yet been
* attached to the UI on the client side.
*
* NOTE: When disabling this after it has been enabled, it will not
* affect {@link DragSourceExtension} and {@link DropTargetExtension} (and
* subclasses) that have been previously added. Those extensions should be
* explicitly removed to make sure user cannot perform DnD operations
* anymore.
*
* @param enabled
* {@code true} if enabled, {@code false} if not
* @since 8.1
*/
public void setMobileHtml5DndEnabled(boolean enabled) {
if (getState(false).enableMobileHTML5DnD != enabled) {
getState().enableMobileHTML5DnD = enabled;
if (isMobileHtml5DndEnabled()) {
if (VaadinService.getCurrentRequest() == null) {
getState().enableMobileHTML5DnD = false;
throw new IllegalStateException("HTML5 DnD cannot be "
+ "enabled for mobile devices when current "
+ "VaadinRequest cannot be accessed. Call this "
+ "method from init(VaadinRequest) to ensure access.");
}
loadMobileHtml5DndPolyfill();
}
}
}
/**
* Load and initialize the mobile drag-drop-polyfill if needed and not yet
* done so.
*/
private void loadMobileHtml5DndPolyfill() {
if (mobileHtml5DndPolyfillLoaded) {
return;
}
if (!getPage().getWebBrowser().isTouchDevice()) {
return;
}
mobileHtml5DndPolyfillLoaded = true;
String vaadinLocation = getSession().getService().getStaticFileLocation(
VaadinService.getCurrentRequest()) + "/VAADIN/";
getPage().addDependency(new Dependency(Type.JAVASCRIPT,
vaadinLocation + ApplicationConstants.MOBILE_DND_POLYFILL_JS));
getRpcProxy(PageClientRpc.class).initializeMobileHtml5DndPolyfill();
}
/**
* Returns whether LayoutManager uses thorough size check that evaluates the
* presence of the element and uses calculated size, or defaults to a
* slightly faster check that can result in incorrect size information if
* the check is triggered while a transform animation is ongoing. This can
* happen e.g. when a PopupView is opened.
*
* By default, the thorough size check is enabled.
*
* @return {@code true} if thorough size check enabled, {@code false} if not
* @since 8.13
*/
public boolean isUsingThoroughSizeCheck() {
return getState(false).thoroughSizeCheck;
}
/**
* Set whether LayoutManager should use thorough size check that evaluates
* the presence of the element and uses calculated size, or default to a
* slightly faster check that can result in incorrect size information if
* the check is triggered while a transform animation is ongoing. This can
* happen e.g. when a PopupView is opened.
*
* By default, the thorough size check is enabled.
*
* @param thoroughSizeCheck
* {@code true} if thorough size check enabled, {@code false} if
* not
* @since 8.13
*/
public void setUsingThoroughSizeCheck(boolean thoroughSizeCheck) {
getState().thoroughSizeCheck = thoroughSizeCheck;
}
/**
* Executes the given callback function after a single client round-trip. If
* this UI gets detached before he callback gets processed, execution is
* cancelled.
*
* @since 8.18
* @param callback
* the callback to trigger after the round-trip
* @return registration object that can be used to cancel the callback
*/
public DelayedCallbackRegistration runAfterRoundTrip(
DelayedCallback callback) {
return runAfterRoundTrip(callback, 1);
}
/**
* Executes the given callback function after the given number of client
* round-trips. If this UI gets detached before he callback gets processed,
* execution is cancelled.
*
* Note: it is not recommended to use a large number as a way of simulating
* a delay.
*
* @since 8.18
* @param callback
* the callback to trigger after the configured round-trips
* @param numberOfRoundTripsToWait
* how many round-trips should be waited for before triggering
* the callback
* @return registration object that can be used to cancel the callback
*/
public DelayedCallbackRegistration runAfterRoundTrip(
DelayedCallback callback, int numberOfRoundTripsToWait) {
return runAfterRoundTrip(callback, null, numberOfRoundTripsToWait);
}
/**
* Executes the given callback function after a single client round-trip, or
* the cancel callback if the execution is cancelled through
* {@link DelayedCallbackRegistration#cancel()}. The framework cancels the
* execution if this UI gets detached before he callback gets processed, but
* the method can also be called by implementing application's code.
*
* @since 8.18
* @param callback
* the callback to trigger after the round-trip
* @param cancelCallback
* the callback to trigger if the execution gets cancelled
* @return registration object that can be used to cancel the callback
*/
public DelayedCallbackRegistration runAfterRoundTrip(
DelayedCallback callback, DelayedCallback cancelCallback) {
return runAfterRoundTrip(callback, 1);
}
/**
* Executes the given callback function after the given number of client
* round-trips, or the cancel callback if the execution is cancelled through
* {@link DelayedCallbackRegistration#cancel()}. The framework cancels the
* execution if this UI gets detached before he callback gets processed, but
* the method can also be called by implementing application's code.
*
* Note: it is not recommended to use a large number or round-trips as a way
* of simulating a delay.
*
* @since 8.18
* @param callback
* the callback to trigger after the configured round-trips
* @param cancelCallback
* the callback to trigger if the execution gets cancelled
* @param numberOfRoundTripsToWait
* how many round-trips should be waited for before triggering
* the callback
* @return registration object that can be used to cancel the callback
*/
public DelayedCallbackRegistration runAfterRoundTrip(
DelayedCallback callback, DelayedCallback cancelCallback,
int numberOfRoundTripsToWait) {
if (numberOfRoundTripsToWait <= 0) {
throw new IllegalArgumentException(
"Number of round-trips must be greater than zero.");
}
InternalDelayedCallbackRegistration registration = new InternalDelayedCallbackRegistration() {
@Override
void increment() {
completed++;
// always remove, even if more round-trips pending
pendingDelayedCallbacks.remove(this);
if (completed < numberOfRoundTripsToWait) {
// new round-trip ahead, update ID and re-add
id = delayedCallbackCounter.incrementAndGet();
pendingDelayedCallbacks.add(this);
// always use the newest, might have increased already
getState().latestDelayedCallbackID = delayedCallbackCounter
.get();
} else {
executed = true;
callback.perform();
}
}
@Override
public boolean cancel() {
if (super.cancel()) {
if (cancelCallback != null) {
cancelCallback.perform();
}
return true;
}
return false;
}
};
pendingDelayedCallbacks.add(registration);
// always use the newest, might have increased already
getState().latestDelayedCallbackID = delayedCallbackCounter.get();
return registration;
}
/**
* An interface for the callback in
* {@link #runAfterRoundTrip(DelayedCallback)}.
*
* Usage examples:
*
* {@code ui.runAfterRoundTrip(() -> textField.focus())}
*
* {@code ui.runAfterRoundTrip(() -> action when successful, () ->
* action when cancelled)}
*
* @since 8.18
*/
public interface DelayedCallback extends Serializable {
public void perform();
}
/**
* An interface used for keeping track of callback requests registered via
* {@link #runAfterRoundTrip(DelayedCallback)}.
*
* @since 8.18
*/
public interface DelayedCallbackRegistration extends Serializable {
/**
* Returns {@code true} if the callback operation is still waiting to be
* executed.
*
* @return {@code true} if callback still waiting, {@code false}
* otherwise
*/
public boolean isPending();
/**
* Returns {@code true} if the delay has been completed and the callback
* operation has been executed.
*
* @return {@code true} if callback executed, {@code false} otherwise
*/
public boolean hasExecuted();
/**
* Returns {@code true} if the callback operation was cancelled
* successfully before it could be executed.
*
* @return {@code true} if cancelled successfully, {@code false}
* otherwise
* @see #cancel()
*/
public boolean isCancelled();
/**
* Cancels the pending callback operation and stops this registration
* object from triggering any further round-trips. Does nothing if the
* callback operation has been executed already.
*
* Note: calling this method does not prevent round-trips getting
* triggered for other reasons.
*
* @return {@code true} if cancelling was successful, {@code false}
* otherwise
*/
public boolean cancel();
/**
* Returns the current ID of this registration object. If the
* registration has been configured to wait for multiple round-trips,
* the ID gets updated for each round.
*
* @return current ID
*/
public long getId();
}
/*
* Private to avoid exposing implementation details or #increment() outside
* of this class.
*/
private abstract class InternalDelayedCallbackRegistration
implements DelayedCallbackRegistration {
protected long id = delayedCallbackCounter.incrementAndGet();
protected boolean cancelled = false;
protected boolean executed = false;
protected int completed = 0;
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public boolean isPending() {
return !executed && !cancelled;
}
@Override
public boolean hasExecuted() {
return executed;
}
@Override
public long getId() {
return id;
}
@Override
public boolean cancel() {
if (!executed) {
cancelled = true;
pendingDelayedCallbacks.remove(this);
return true;
}
return false;
}
abstract void increment();
}
/**
* Event which is fired when the ordering of the windows is updated.
*
* The other way to listen window position for specific window is
* {@link Window#addWindowOrderChangeListener(WindowOrderChangeListener)}
*
* @see Window.WindowOrderChangeEvent
*
* @author Vaadin Ltd
* @since 8.0
*
*/
public static class WindowOrderUpdateEvent extends Component.Event {
private final Collection windows;
public WindowOrderUpdateEvent(Component source,
Collection windows) {
super(source);
this.windows = windows;
}
/**
* Gets the windows in the order they appear in the UI: top most window
* is first, bottom one last.
*
* @return the windows collection
*/
public Collection getWindows() {
return windows;
}
}
/**
* An interface used for listening to Windows order update events.
*
* @since 8.0
*
* @see Window.WindowOrderChangeEvent
*/
@FunctionalInterface
public interface WindowOrderUpdateListener extends ConnectorEventListener {
public static final Method windowOrderUpdateMethod = ReflectTools
.findMethod(WindowOrderUpdateListener.class,
"windowOrderUpdated", WindowOrderUpdateEvent.class);
/**
* Called when the windows order positions are changed. Use
* {@link WindowOrderUpdateEvent#getWindows()} to get a reference to the
* {@link Window}s whose order positions are updated. Use
* {@link Window#getOrderPosition()} to get window position for specific
* window.
*
* @param event
*/
public void windowOrderUpdated(WindowOrderUpdateEvent event);
}
}