/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.client.ui; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Focusable; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HasEnabled; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ComponentConnector; import com.vaadin.client.DeferredWorker; import com.vaadin.client.VCaptionWrapper; import com.vaadin.client.VConsole; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.client.ui.popupview.VisibilityChangeEvent; import com.vaadin.client.ui.popupview.VisibilityChangeHandler; public class VPopupView extends HTML implements HasEnabled, Iterable<Widget>, DeferredWorker { public static final String CLASSNAME = "v-popupview"; /** * For server-client communication. * <p> * For internal use only. May be removed or replaced in the future. */ public String uidlId; /** For internal use only. May be removed or replaced in the future. */ public ApplicationConnection client; /** * Helps to communicate popup visibility to the server. * <p> * For internal use only. May be removed or replaced in the future. */ public boolean hostPopupVisible; /** For internal use only. May be removed or replaced in the future. */ public final CustomPopup popup; private final Label loading = new Label(); private boolean popupShowInProgress; private boolean enabled = true; /** * loading constructor */ public VPopupView() { super(); popup = new CustomPopup(); setStyleName(CLASSNAME); popup.setStyleName(CLASSNAME + "-popup"); loading.setStyleName(CLASSNAME + "-loading"); setHTML(""); popup.setWidget(loading); // When we click to open the popup... addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { if (isEnabled()) { preparePopup(popup); showPopup(popup); center(); fireEvent(new VisibilityChangeEvent(true)); } } }); // ..and when we close it popup.addCloseHandler(new CloseHandler<PopupPanel>() { @Override public void onClose(CloseEvent<PopupPanel> event) { fireEvent(new VisibilityChangeEvent(false)); } }); // TODO: Enable animations once GWT fix has been merged popup.setAnimationEnabled(false); popup.setAutoHideOnHistoryEventsEnabled(false); } /** For internal use only. May be removed or replaced in the future. */ public void preparePopup(final CustomPopup popup) { popup.setVisible(true); popup.setWidget(loading); popup.show(); } /** * Determines the correct position for a popup and displays the popup at * that position. * * By default, the popup is shown centered relative to its host component, * ensuring it is visible on the screen if possible. * * Can be overridden to customize the popup position. * * @param popup */ public void showPopup(final CustomPopup popup) { popup.setPopupPosition(0, 0); } /** For internal use only. May be removed or replaced in the future. */ public void center() { int windowTop = RootPanel.get().getAbsoluteTop(); int windowLeft = RootPanel.get().getAbsoluteLeft(); int windowRight = windowLeft + RootPanel.get().getOffsetWidth(); int windowBottom = windowTop + RootPanel.get().getOffsetHeight(); int offsetWidth = popup.getOffsetWidth(); int offsetHeight = popup.getOffsetHeight(); int hostHorizontalCenter = VPopupView.this.getAbsoluteLeft() + VPopupView.this.getOffsetWidth() / 2; int hostVerticalCenter = VPopupView.this.getAbsoluteTop() + VPopupView.this.getOffsetHeight() / 2; int left = hostHorizontalCenter - offsetWidth / 2; int top = hostVerticalCenter - offsetHeight / 2; // Don't show the popup outside the screen. if ((left + offsetWidth) > windowRight) { left -= (left + offsetWidth) - windowRight; } if ((top + offsetHeight) > windowBottom) { top -= (top + offsetHeight) - windowBottom; } if (left < 0) { left = 0; } if (top < 0) { top = 0; } popup.setPopupPosition(left, top); } /** * Make sure that we remove the popup when the main widget is removed. * * @see com.google.gwt.user.client.ui.Widget#onUnload() */ @Override protected void onDetach() { popup.hide(); super.onDetach(); } private static native void nativeBlur(Element e) /*-{ if(e && e.blur) { e.blur(); } }-*/; /** * Returns true if the popup is enabled, false if not. * * @since 7.3.4 */ @Override public boolean isEnabled() { return enabled; } /** * Sets whether this popup is enabled. * * @param enabled * <code>true</code> to enable the popup, <code>false</code> to * disable it * @since 7.3.4 */ @Override public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * This class is only public to enable overriding showPopup, and is * currently not intended to be extended or otherwise used directly. Its API * (other than it being a VOverlay) is to be considered private and * potentially subject to change. */ public class CustomPopup extends VOverlay implements StateChangeEvent.StateChangeHandler { private ComponentConnector popupComponentConnector = null; /** For internal use only. May be removed or replaced in the future. */ public Widget popupComponentWidget = null; /** For internal use only. May be removed or replaced in the future. */ public VCaptionWrapper captionWrapper = null; private boolean hasHadMouseOver = false; private boolean hideOnMouseOut = true; private final Set<Element> activeChildren = new HashSet<>(); private ShortcutActionHandler shortcutActionHandler; public CustomPopup() { super(true, false); // autoHide, not modal setOwner(VPopupView.this); // Delegate popup keyboard events to the relevant handler. The // events do not propagate automatically because the popup is // directly attached to the RootPanel. addDomHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { if (shortcutActionHandler != null) { shortcutActionHandler.handleKeyboardEvent( Event.as(event.getNativeEvent())); } } }, KeyDownEvent.getType()); } // For some reason ONMOUSEOUT events are not always received, so we have // to use ONMOUSEMOVE that doesn't target the popup @Override public boolean onEventPreview(Event event) { Element target = DOM.eventGetTarget(event); boolean eventTargetsPopup = DOM.isOrHasChild(getElement(), target); int type = DOM.eventGetType(event); // Catch children that use keyboard, so we can unfocus them when // hiding if (eventTargetsPopup && type == Event.ONKEYPRESS) { activeChildren.add(target); } if (eventTargetsPopup && type == Event.ONMOUSEMOVE) { hasHadMouseOver = true; } if (!eventTargetsPopup && type == Event.ONMOUSEMOVE) { if (hasHadMouseOver && hideOnMouseOut) { hide(); return true; } } // Was the TAB key released outside of our popup? if (!eventTargetsPopup && type == Event.ONKEYUP && event.getKeyCode() == KeyCodes.KEY_TAB) { // Should we hide on focus out (mouse out)? if (hideOnMouseOut) { hide(); return true; } } return super.onEventPreview(event); } @Override public void hide(boolean autoClosed) { VConsole.log("Hiding popupview"); syncChildren(); clearPopupComponentConnector(); hasHadMouseOver = false; shortcutActionHandler = null; super.hide(autoClosed); } @Override public void show() { popupShowInProgress = true; // Find the shortcut action handler that should handle keyboard // events from the popup. The events do not propagate automatically // because the popup is directly attached to the RootPanel. super.show(); /* * Shortcut actions could be set (and currently in 7.2 they ARE SET * via old style "updateFromUIDL" method, see f.e. UIConnector) * AFTER method show() has been invoked (which is called as a * reaction on change in component hierarchy). As a result there * could be no shortcutActionHandler set yet. So let's postpone * search of shortcutActionHandler. */ Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { try { if (shortcutActionHandler == null) { shortcutActionHandler = findShortcutActionHandler(); } } finally { popupShowInProgress = false; } } }); } /** * Try to sync all known active child widgets to server. */ public void syncChildren() { // Notify children with focus if ((popupComponentWidget instanceof Focusable)) { ((Focusable) popupComponentWidget).setFocus(false); } // Notify children that have used the keyboard for (Element e : activeChildren) { try { nativeBlur(e); } catch (Exception ignored) { } } activeChildren.clear(); } private void clearPopupComponentConnector() { if (popupComponentConnector != null) { popupComponentConnector.removeStateChangeHandler(this); } popupComponentConnector = null; popupComponentWidget = null; captionWrapper = null; } @Override public boolean remove(Widget w) { clearPopupComponentConnector(); return super.remove(w); } public void setPopupConnector(ComponentConnector newPopupComponent) { if (newPopupComponent != popupComponentConnector) { if (popupComponentConnector != null) { popupComponentConnector.removeStateChangeHandler(this); } Widget newWidget = newPopupComponent.getWidget(); setWidget(newWidget); popupComponentWidget = newWidget; popupComponentConnector = newPopupComponent; popupComponentConnector.addStateChangeHandler("height", this); popupComponentConnector.addStateChangeHandler("width", this); } } public void setHideOnMouseOut(boolean hideOnMouseOut) { this.hideOnMouseOut = hideOnMouseOut; } @Override public com.google.gwt.user.client.Element getContainerElement() { return super.getContainerElement(); } @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { positionOrSizeUpdated(); } private ShortcutActionHandler findShortcutActionHandler() { Widget widget = VPopupView.this; ShortcutActionHandler handler = null; while (handler == null && widget != null) { if (widget instanceof ShortcutActionHandlerOwner) { handler = ((ShortcutActionHandlerOwner) widget) .getShortcutActionHandler(); } widget = widget.getParent(); } return handler; } }// class CustomPopup public HandlerRegistration addVisibilityChangeHandler( final VisibilityChangeHandler visibilityChangeHandler) { return addHandler(visibilityChangeHandler, VisibilityChangeEvent.getType()); } @Override public Iterator<Widget> iterator() { return Collections.singleton((Widget) popup).iterator(); } /** * Checks whether there are operations pending for this widget that must be * executed before reaching a steady state. * * @returns <code>true</code> iff there are operations pending which must be * executed before reaching a steady state * @since 7.3.4 */ @Override public boolean isWorkPending() { return popupShowInProgress; } }// class VPopupView