/* * Copyright 2011 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.terminal.gwt.client.ui; 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.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style; import com.google.gwt.event.dom.client.DomEvent.Type; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.BrowserInfo; import com.vaadin.terminal.gwt.client.Container; import com.vaadin.terminal.gwt.client.Focusable; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.RenderInformation; import com.vaadin.terminal.gwt.client.RenderSpace; import com.vaadin.terminal.gwt.client.UIDL; import com.vaadin.terminal.gwt.client.Util; import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; public class VPanel extends SimplePanel implements Container, ShortcutActionHandlerOwner, Focusable { public static final String CLICK_EVENT_IDENTIFIER = "click"; public static final String CLASSNAME = "v-panel"; ApplicationConnection client; String id; private final Element captionNode = DOM.createDiv(); private final Element captionText = DOM.createSpan(); private Icon icon; private final Element bottomDecoration = DOM.createDiv(); private final Element contentNode = DOM.createDiv(); private Element errorIndicatorElement; private String height; private Paintable layout; ShortcutActionHandler shortcutHandler; private String width = ""; private Element geckoCaptionMeter; private int scrollTop; private int scrollLeft; private RenderInformation renderInformation = new RenderInformation(); private int borderPaddingHorizontal = -1; private int borderPaddingVertical = -1; private int captionPaddingHorizontal = -1; private int captionMarginLeft = -1; private boolean rendering; private int contentMarginLeft = -1; private String previousStyleName; private final TouchScrollHandler touchScrollHandler; private ClickEventHandler clickEventHandler = new ClickEventHandler(this, CLICK_EVENT_IDENTIFIER) { @Override protected HandlerRegistration registerHandler( H handler, Type type) { return addDomHandler(handler, type); } }; public VPanel() { super(); DivElement captionWrap = Document.get().createDivElement(); captionWrap.appendChild(captionNode); captionNode.appendChild(captionText); captionWrap.setClassName(CLASSNAME + "-captionwrap"); captionNode.setClassName(CLASSNAME + "-caption"); contentNode.setClassName(CLASSNAME + "-content"); bottomDecoration.setClassName(CLASSNAME + "-deco"); getElement().appendChild(captionWrap); /* * Make contentNode focusable only by using the setFocus() method. This * behaviour can be changed by invoking setTabIndex() in the serverside * implementation */ contentNode.setTabIndex(-1); getElement().appendChild(contentNode); getElement().appendChild(bottomDecoration); setStyleName(CLASSNAME); DOM.sinkEvents(getElement(), Event.ONKEYDOWN); DOM.sinkEvents(contentNode, Event.ONSCROLL | Event.TOUCHEVENTS); contentNode.getStyle().setProperty("position", "relative"); getElement().getStyle().setProperty("overflow", "hidden"); touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this, contentNode); } /** * Sets the keyboard focus on the Panel * * @param focus * Should the panel have focus or not. */ public void setFocus(boolean focus) { if (focus) { getContainerElement().focus(); } else { getContainerElement().blur(); } } /* * (non-Javadoc) * * @see com.vaadin.terminal.gwt.client.Focusable#focus() */ public void focus() { setFocus(true); } @Override protected Element getContainerElement() { return contentNode; } private void setCaption(String text) { DOM.setInnerHTML(captionText, text); } public void updateFromUIDL(UIDL uidl, final ApplicationConnection client) { rendering = true; if (!uidl.hasAttribute("cached")) { // Handle caption displaying and style names, prior generics. // Affects size calculations final boolean hasCaption = uidl.hasAttribute("caption") && !uidl.getStringAttribute("caption").equals(""); setCaption(hasCaption ? uidl.getStringAttribute("caption") : ""); // Restore default stylenames final String captionBaseClass = CLASSNAME + (hasCaption ? "-caption" : "-nocaption"); final String contentBaseClass = CLASSNAME + "-content"; final String decoBaseClass = CLASSNAME + "-deco"; captionNode.setClassName(captionBaseClass); contentNode.setClassName(contentBaseClass); bottomDecoration.setClassName(decoBaseClass); // Add proper stylenames for all elements. This way we can prevent // unwanted CSS selector inheritance. if (uidl.hasAttribute("style")) { final String[] styles = uidl.getStringAttribute("style").split( " "); for (String style : styles) { captionNode.addClassName(captionBaseClass + "-" + style); contentNode.addClassName(contentBaseClass + "-" + style); bottomDecoration.addClassName(decoBaseClass + "-" + style); } } // Ensure panel is still scrollable touchScrollHandler.addElement(contentNode); } // Ensure correct implementation if (client.updateComponent(this, uidl, false)) { rendering = false; return; } clickEventHandler.handleEventHandlerRegistration(client); this.client = client; id = uidl.getId(); setIconUri(uidl, client); handleError(uidl); // Render content final UIDL layoutUidl = uidl.getChildUIDL(0); final Paintable newLayout = client.getPaintable(layoutUidl); if (newLayout != layout) { if (layout != null) { client.unregisterPaintable(layout); } setWidget((Widget) newLayout); layout = newLayout; } layout.updateFromUIDL(layoutUidl, client); // We may have actions attached to this panel if (uidl.getChildCount() > 1) { final int cnt = uidl.getChildCount(); for (int i = 1; i < cnt; i++) { UIDL childUidl = uidl.getChildUIDL(i); if (childUidl.getTag().equals("actions")) { if (shortcutHandler == null) { shortcutHandler = new ShortcutActionHandler(id, client); } shortcutHandler.updateActionMap(childUidl); } } } if (uidl.hasVariable("scrollTop") && uidl.getIntVariable("scrollTop") != scrollTop) { scrollTop = uidl.getIntVariable("scrollTop"); contentNode.setScrollTop(scrollTop); // re-read the actual scrollTop in case invalid value was set // (scrollTop != 0 when no scrollbar exists, other values would be // caught by scroll listener), see #3784 scrollTop = contentNode.getScrollTop(); } if (uidl.hasVariable("scrollLeft") && uidl.getIntVariable("scrollLeft") != scrollLeft) { scrollLeft = uidl.getIntVariable("scrollLeft"); contentNode.setScrollLeft(scrollLeft); // re-read the actual scrollTop in case invalid value was set // (scrollTop != 0 when no scrollbar exists, other values would be // caught by scroll listener), see #3784 scrollLeft = contentNode.getScrollLeft(); } // Must be run after scrollTop is set as Webkit overflow fix re-sets the // scrollTop runHacks(false); // And apply tab index if (uidl.hasVariable("tabindex")) { contentNode.setTabIndex(uidl.getIntVariable("tabindex")); } if ((BrowserInfo.get().isWebkit() || BrowserInfo.get().isOpera()) && Util.getNativeScrollbarSize() > 0 && getWidget().getOffsetWidth() > contentNode.getOffsetWidth()) { /* * Most likely due to some timing issues with webkit overflow scroll * hack causes that scrollbars are not taken into account when * calculating content size. See #10255 */ Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, VPanel.this); } }); } rendering = false; } @Override public void setStyleName(String style) { if (!style.equals(previousStyleName)) { super.setStyleName(style); detectContainerBorders(); previousStyleName = style; } } private void handleError(UIDL uidl) { if (uidl.hasAttribute("error")) { if (errorIndicatorElement == null) { errorIndicatorElement = DOM.createSpan(); DOM.setElementProperty(errorIndicatorElement, "className", "v-errorindicator"); DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS); sinkEvents(Event.MOUSEEVENTS); } DOM.insertBefore(captionNode, errorIndicatorElement, captionText); } else if (errorIndicatorElement != null) { DOM.removeChild(captionNode, errorIndicatorElement); errorIndicatorElement = null; } } private void setIconUri(UIDL uidl, ApplicationConnection client) { final String iconUri = uidl.hasAttribute("icon") ? uidl .getStringAttribute("icon") : null; if (iconUri == null) { if (icon != null) { DOM.removeChild(captionNode, icon.getElement()); icon = null; } } else { if (icon == null) { icon = new Icon(client); DOM.insertChild(captionNode, icon.getElement(), 0); } icon.setUri(iconUri); } } public void runHacks(boolean runGeckoFix) { if (BrowserInfo.get().isIE6() && width != null && !width.equals("")) { /* * IE6 requires overflow-hidden elements to have a width specified * so we calculate the width of the content and caption nodes when * no width has been specified. */ /* * Fixes #1923 VPanel: Horizontal scrollbar does not appear in IE6 * with wide content */ /* * Caption must be shrunk for parent measurements to return correct * result in IE6 */ DOM.setStyleAttribute(captionNode, "width", "1px"); int parentPadding = Util.measureHorizontalPaddingAndBorder( getElement(), 0); int parentWidthExcludingPadding = getElement().getOffsetWidth() - parentPadding; Util.setWidthExcludingPaddingAndBorder(captionNode, parentWidthExcludingPadding - getCaptionMarginLeft(), 26, false); int contentMarginLeft = getContentMarginLeft(); Util.setWidthExcludingPaddingAndBorder(contentNode, parentWidthExcludingPadding - contentMarginLeft, 2, false); } if ((BrowserInfo.get().isIE() || BrowserInfo.get().isFF2()) && (width == null || width.equals(""))) { /* * IE and FF2 needs width to be specified for the root DIV so we * calculate that from the sizes of the caption and layout */ int captionWidth = captionText.getOffsetWidth() + getCaptionMarginLeft() + getCaptionPaddingHorizontal(); int layoutWidth = ((Widget) layout).getOffsetWidth() + getContainerBorderWidth(); int width = layoutWidth; if (captionWidth > width) { width = captionWidth; } if (BrowserInfo.get().isIE7()) { Util.setWidthExcludingPaddingAndBorder(captionNode, width - getCaptionMarginLeft(), 26, false); } super.setWidth(width + "px"); } if (runGeckoFix && BrowserInfo.get().isGecko()) { // workaround for #1764 if (width == null || width.equals("")) { if (geckoCaptionMeter == null) { geckoCaptionMeter = DOM.createDiv(); DOM.appendChild(captionNode, geckoCaptionMeter); } int captionWidth = DOM.getElementPropertyInt(captionText, "offsetWidth"); int availWidth = DOM.getElementPropertyInt(geckoCaptionMeter, "offsetWidth"); if (captionWidth == availWidth) { /* * Caption width defines panel width -> Gecko based browsers * somehow fails to float things right, without the * "noncode" below */ setWidth(getOffsetWidth() + "px"); } else { DOM.setStyleAttribute(captionNode, "width", ""); } } } client.runDescendentsLayout(this); Util.runWebkitOverflowAutoFix(contentNode); } @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); final Element target = DOM.eventGetTarget(event); final int type = DOM.eventGetType(event); if (type == Event.ONKEYDOWN && shortcutHandler != null) { shortcutHandler.handleKeyboardEvent(event); return; } if (type == Event.ONSCROLL) { int newscrollTop = DOM.getElementPropertyInt(contentNode, "scrollTop"); int newscrollLeft = DOM.getElementPropertyInt(contentNode, "scrollLeft"); if (client != null && (newscrollLeft != scrollLeft || newscrollTop != scrollTop)) { scrollLeft = newscrollLeft; scrollTop = newscrollTop; client.updateVariable(id, "scrollTop", scrollTop, false); client.updateVariable(id, "scrollLeft", scrollLeft, false); } } else if (captionNode.isOrHasChild(target)) { if (client != null) { client.handleTooltipEvent(event, this); } } } @Override public void setHeight(String height) { this.height = height; super.setHeight(height); if (height != null && !"".equals(height)) { final int targetHeight = getOffsetHeight(); int containerHeight = targetHeight - captionNode.getParentElement().getOffsetHeight() - bottomDecoration.getOffsetHeight() - getContainerBorderHeight(); if (containerHeight < 0) { containerHeight = 0; } DOM.setStyleAttribute(contentNode, "height", containerHeight + "px"); } else { DOM.setStyleAttribute(contentNode, "height", ""); } if (!rendering) { runHacks(true); } } private int getCaptionMarginLeft() { if (captionMarginLeft < 0) { detectContainerBorders(); } return captionMarginLeft; } private int getContentMarginLeft() { if (contentMarginLeft < 0) { detectContainerBorders(); } return contentMarginLeft; } private int getCaptionPaddingHorizontal() { if (captionPaddingHorizontal < 0) { detectContainerBorders(); } return captionPaddingHorizontal; } private int getContainerBorderHeight() { if (borderPaddingVertical < 0) { detectContainerBorders(); } return borderPaddingVertical; } @Override public void setWidth(String width) { if (this.width.equals(width)) { return; } this.width = width; super.setWidth(width); if (!rendering) { runHacks(true); if (height.equals("")) { // Width change may affect height Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this); } } } private int getContainerBorderWidth() { if (borderPaddingHorizontal < 0) { detectContainerBorders(); } return borderPaddingHorizontal; } private void detectContainerBorders() { Style contentStyle = contentNode.getStyle(); String overflow = contentStyle.getProperty("overflow"); contentStyle.setProperty("overflow", "hidden"); borderPaddingHorizontal = Util.measureHorizontalBorder(contentNode); borderPaddingVertical = Util.measureVerticalBorder(contentNode); contentStyle.setProperty("overflow", overflow); captionPaddingHorizontal = Util.measureHorizontalPaddingAndBorder( captionNode, 26); captionMarginLeft = Util.measureMarginLeft(captionNode); contentMarginLeft = Util.measureMarginLeft(contentNode); } public boolean hasChildComponent(Widget component) { if (component != null && component == layout) { return true; } else { return false; } } public void replaceChildComponent(Widget oldComponent, Widget newComponent) { // TODO This is untested as no layouts require this if (oldComponent != layout) { return; } setWidget(newComponent); layout = (Paintable) newComponent; } public RenderSpace getAllocatedSpace(Widget child) { int w = 0; int h = 0; if (width != null && !width.equals("")) { w = getOffsetWidth() - getContainerBorderWidth(); if (w < 0) { w = 0; } } if (height != null && !height.equals("")) { h = contentNode.getOffsetHeight() - getContainerBorderHeight(); if (h < 0) { h = 0; } } return new RenderSpace(w, h, true); } public boolean requestLayout(Set child) { // content size change might cause change to its available space // (scrollbars) client.handleComponentRelativeSize((Widget) layout); if (height != null && height != "" && width != null && width != "") { /* * If the height and width has been specified the child components * cannot make the size of the layout change */ return true; } runHacks(false); return !renderInformation.updateSize(getElement()); } public void updateCaption(Paintable component, UIDL uidl) { // NOP: layouts caption, errors etc not rendered in Panel } @Override protected void onAttach() { super.onAttach(); detectContainerBorders(); } public ShortcutActionHandler getShortcutActionHandler() { return shortcutHandler; } }