Java tutorial
/* @VaadinApache2LicenseForJavaFiles@ */ package com.vaadin.terminal.gwt.client.ui; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import com.google.gwt.dom.client.ImageElement; import com.google.gwt.dom.client.NodeList; 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.ComplexPanel; 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.ContainerResizedListener; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; 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.VCaption; import com.vaadin.terminal.gwt.client.VCaptionWrapper; /** * Custom Layout implements complex layout defined with HTML template. * * @author Vaadin Ltd * */ public class VCustomLayout extends ComplexPanel implements Paintable, Container, ContainerResizedListener { public static final String CLASSNAME = "v-customlayout"; /** Location-name to containing element in DOM map */ private final HashMap<String, Element> locationToElement = new HashMap<String, Element>(); /** Location-name to contained widget map */ private final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>(); /** Widget to captionwrapper map */ private final HashMap<Paintable, VCaptionWrapper> widgetToCaptionWrapper = new HashMap<Paintable, VCaptionWrapper>(); /** Name of the currently rendered style */ String currentTemplateName; /** Unexecuted scripts loaded from the template */ private String scripts = ""; /** Paintable ID of this paintable */ private String pid; private ApplicationConnection client; /** Has the template been loaded from contents passed in UIDL **/ private boolean hasTemplateContents = false; private Element elementWithNativeResizeFunction; private String height = ""; private String width = ""; private HashMap<String, FloatSize> locationToExtraSize = new HashMap<String, FloatSize>(); public VCustomLayout() { setElement(DOM.createDiv()); // Clear any unwanted styling DOM.setStyleAttribute(getElement(), "border", "none"); DOM.setStyleAttribute(getElement(), "margin", "0"); DOM.setStyleAttribute(getElement(), "padding", "0"); if (BrowserInfo.get().isIE()) { DOM.setStyleAttribute(getElement(), "position", "relative"); } setStyleName(CLASSNAME); } /** * Sets widget to given location. * * If location already contains a widget it will be removed. * * @param widget * Widget to be set into location. * @param location * location name where widget will be added * * @throws IllegalArgumentException * if no such location is found in the layout. */ public void setWidget(Widget widget, String location) { if (widget == null) { return; } // If no given location is found in the layout, and exception is throws Element elem = locationToElement.get(location); if (elem == null && hasTemplate()) { throw new IllegalArgumentException("No location " + location + " found"); } // Get previous widget final Widget previous = locationToWidget.get(location); // NOP if given widget already exists in this location if (previous == widget) { return; } if (previous != null) { remove(previous); } // if template is missing add element in order if (!hasTemplate()) { elem = getElement(); } // Add widget to location super.add(widget, elem); locationToWidget.put(location, widget); } /** Update the layout from UIDL */ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { this.client = client; // ApplicationConnection manages generic component features if (client.updateComponent(this, uidl, true)) { return; } pid = uidl.getId(); if (!hasTemplate()) { // Update HTML template only once initializeHTML(uidl, client); } // Evaluate scripts eval(scripts); scripts = null; iLayout(); // TODO Check if this is needed client.runDescendentsLayout(this); Set<Widget> oldWidgets = new HashSet<Widget>(); oldWidgets.addAll(locationToWidget.values()); // For all contained widgets for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { final UIDL uidlForChild = (UIDL) i.next(); if (uidlForChild.getTag().equals("location")) { final String location = uidlForChild.getStringAttribute("name"); final Paintable child = client.getPaintable(uidlForChild.getChildUIDL(0)); try { setWidget((Widget) child, location); child.updateFromUIDL(uidlForChild.getChildUIDL(0), client); } catch (final IllegalArgumentException e) { // If no location is found, this component is not visible } oldWidgets.remove(child); } } for (Iterator<Widget> iterator = oldWidgets.iterator(); iterator.hasNext();) { Widget oldWidget = iterator.next(); if (oldWidget.isAttached()) { // slot of this widget is emptied, remove it remove(oldWidget); } } iLayout(); // TODO Check if this is needed client.runDescendentsLayout(this); } /** Initialize HTML-layout. */ private void initializeHTML(UIDL uidl, ApplicationConnection client) { final String newTemplateContents = uidl.getStringAttribute("templateContents"); final String newTemplate = uidl.getStringAttribute("template"); currentTemplateName = null; hasTemplateContents = false; String template = ""; if (newTemplate != null) { // Get the HTML-template from client template = client.getResource("layouts/" + newTemplate + ".html"); if (template == null) { template = "<em>Layout file layouts/" + newTemplate + ".html is missing. Components will be drawn for debug purposes.</em>"; } else { currentTemplateName = newTemplate; } } else { hasTemplateContents = true; template = newTemplateContents; } // Connect body of the template to DOM template = extractBodyAndScriptsFromTemplate(template); // TODO prefix img src:s here with a regeps, cannot work further with IE String themeUri = client.getThemeUri(); String relImgPrefix = themeUri + "/layouts/"; // prefix all relative image elements to point to theme dir with a // regexp search template = template.replaceAll("<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"", "<$1 $2src=\"" + relImgPrefix + "$3\""); // also support src attributes without quotes template = template.replaceAll("<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]", "<$1 $2src=\"" + relImgPrefix + "$3\""); // also prefix relative style="...url(...)..." template = template.replaceAll("(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)", "$1 " + relImgPrefix + "$2 $3"); getElement().setInnerHTML(template); // Remap locations to elements locationToElement.clear(); scanForLocations(getElement()); initImgElements(); elementWithNativeResizeFunction = DOM.getFirstChild(getElement()); if (elementWithNativeResizeFunction == null) { elementWithNativeResizeFunction = getElement(); } publishResizedFunction(elementWithNativeResizeFunction); } private native boolean uriEndsWithSlash() /*-{ var path = $wnd.location.pathname; if(path.charAt(path.length - 1) == "/") return true; return false; }-*/; private boolean hasTemplate() { if (currentTemplateName == null && !hasTemplateContents) { return false; } else { return true; } } /** Collect locations from template */ private void scanForLocations(Element elem) { final String location = elem.getAttribute("location"); if (!"".equals(location)) { locationToElement.put(location, elem); elem.setInnerHTML(""); int x = Util.measureHorizontalPaddingAndBorder(elem, 0); int y = Util.measureVerticalPaddingAndBorder(elem, 0); FloatSize fs = new FloatSize(x, y); locationToExtraSize.put(location, fs); } else { final int len = DOM.getChildCount(elem); for (int i = 0; i < len; i++) { scanForLocations(DOM.getChild(elem, i)); } } } /** Evaluate given script in browser document */ private static native void eval(String script) /*-{ try { if (script != null) eval("{ var document = $doc; var window = $wnd; "+ script + "}"); } catch (e) { } }-*/; /** * Img elements needs some special handling in custom layout. Img elements * will get their onload events sunk. This way custom layout can notify * parent about possible size change. */ private void initImgElements() { NodeList<com.google.gwt.dom.client.Element> nodeList = getElement().getElementsByTagName("IMG"); for (int i = 0; i < nodeList.getLength(); i++) { com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList.getItem(i); DOM.sinkEvents((Element) img.cast(), Event.ONLOAD); } } /** * Extract body part and script tags from raw html-template. * * Saves contents of all script-tags to private property: scripts. Returns * contents of the body part for the html without script-tags. Also replaces * all _UID_ tags with an unique id-string. * * @param html * Original HTML-template received from server * @return html that is used to create the HTMLPanel. */ private String extractBodyAndScriptsFromTemplate(String html) { // Replace UID:s html = html.replaceAll("_UID_", pid + "__"); // Exctract script-tags scripts = ""; int endOfPrevScript = 0; int nextPosToCheck = 0; String lc = html.toLowerCase(); String res = ""; int scriptStart = lc.indexOf("<script", nextPosToCheck); while (scriptStart > 0) { res += html.substring(endOfPrevScript, scriptStart); scriptStart = lc.indexOf(">", scriptStart); final int j = lc.indexOf("</script>", scriptStart); scripts += html.substring(scriptStart + 1, j) + ";"; nextPosToCheck = endOfPrevScript = j + "</script>".length(); scriptStart = lc.indexOf("<script", nextPosToCheck); } res += html.substring(endOfPrevScript); // Extract body html = res; lc = html.toLowerCase(); int startOfBody = lc.indexOf("<body"); if (startOfBody < 0) { res = html; } else { res = ""; startOfBody = lc.indexOf(">", startOfBody) + 1; final int endOfBody = lc.indexOf("</body>", startOfBody); if (endOfBody > startOfBody) { res = html.substring(startOfBody, endOfBody); } else { res = html.substring(startOfBody); } } return res; } /** Replace child components */ public void replaceChildComponent(Widget from, Widget to) { final String location = getLocation(from); if (location == null) { throw new IllegalArgumentException(); } setWidget(to, location); } /** Does this layout contain given child */ public boolean hasChildComponent(Widget component) { return locationToWidget.containsValue(component); } /** Update caption for given widget */ public void updateCaption(Paintable component, UIDL uidl) { VCaptionWrapper wrapper = widgetToCaptionWrapper.get(component); if (VCaption.isNeeded(uidl)) { if (wrapper == null) { final String loc = getLocation((Widget) component); super.remove((Widget) component); wrapper = new VCaptionWrapper(component, client); super.add(wrapper, locationToElement.get(loc)); widgetToCaptionWrapper.put(component, wrapper); } wrapper.updateCaption(uidl); } else { if (wrapper != null) { final String loc = getLocation((Widget) component); super.remove(wrapper); super.add((Widget) wrapper.getPaintable(), locationToElement.get(loc)); widgetToCaptionWrapper.remove(component); } } } /** Get the location of an widget */ public String getLocation(Widget w) { for (final Iterator<String> i = locationToWidget.keySet().iterator(); i.hasNext();) { final String location = i.next(); if (locationToWidget.get(location) == w) { return location; } } return null; } /** Removes given widget from the layout */ @Override public boolean remove(Widget w) { client.unregisterPaintable((Paintable) w); final String location = getLocation(w); if (location != null) { locationToWidget.remove(location); } final VCaptionWrapper cw = widgetToCaptionWrapper.get(w); if (cw != null) { widgetToCaptionWrapper.remove(w); return super.remove(cw); } else if (w != null) { return super.remove(w); } return false; } /** Adding widget without specifying location is not supported */ @Override public void add(Widget w) { throw new UnsupportedOperationException(); } /** Clear all widgets from the layout */ @Override public void clear() { super.clear(); locationToWidget.clear(); widgetToCaptionWrapper.clear(); } public void iLayout() { iLayoutJS(DOM.getFirstChild(getElement())); } /** * This method is published to JS side with the same name into first DOM * node of custom layout. This way if one implements some resizeable * containers in custom layout he/she can notify children after resize. */ public void notifyChildrenOfSizeChange() { client.runDescendentsLayout(this); } @Override public void onDetach() { super.onDetach(); if (elementWithNativeResizeFunction != null) { detachResizedFunction(elementWithNativeResizeFunction); } } private native void detachResizedFunction(Element element) /*-{ element.notifyChildrenOfSizeChange = null; }-*/; private native void publishResizedFunction(Element element) /*-{ var self = this; element.notifyChildrenOfSizeChange = function() { self.@com.vaadin.terminal.gwt.client.ui.VCustomLayout::notifyChildrenOfSizeChange()(); }; }-*/; /** * In custom layout one may want to run layout functions made with * JavaScript. This function tests if one exists (with name "iLayoutJS" in * layouts first DOM node) and runs et. Return value is used to determine if * children needs to be notified of size changes. * * Note! When implementing a JS layout function you most likely want to call * notifyChildrenOfSizeChange() function on your custom layouts main * element. That method is used to control whether child components layout * functions are to be run. * * @param el * @return true if layout function exists and was run successfully, else * false. */ private native boolean iLayoutJS(Element el) /*-{ if(el && el.iLayoutJS) { try { el.iLayoutJS(); return true; } catch (e) { return false; } } else { return false; } }-*/; public boolean requestLayout(Set<Paintable> child) { updateRelativeSizedComponents(true, true); if (width.equals("") || height.equals("")) { /* Automatically propagated upwards if the size can change */ return false; } return true; } public RenderSpace getAllocatedSpace(Widget child) { com.google.gwt.dom.client.Element pe = child.getElement().getParentElement(); FloatSize extra = locationToExtraSize.get(getLocation(child)); return new RenderSpace(pe.getOffsetWidth() - (int) extra.getWidth(), pe.getOffsetHeight() - (int) extra.getHeight(), Util.mayHaveScrollBars(pe)); } @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); if (event.getTypeInt() == Event.ONLOAD) { Util.notifyParentOfSizeChange(this, true); event.cancelBubble(true); } } @Override public void setHeight(String height) { if (this.height.equals(height)) { return; } boolean shrinking = true; if (isLarger(height, this.height)) { shrinking = false; } this.height = height; super.setHeight(height); /* * If the height shrinks we must remove all components with relative * height from the DOM, update their height when they do not affect the * available space and finally restore them to the original state */ if (shrinking) { updateRelativeSizedComponents(false, true); } } @Override public void setWidth(String width) { if (this.width.equals(width)) { return; } boolean shrinking = true; if (isLarger(width, this.width)) { shrinking = false; } super.setWidth(width); this.width = width; /* * If the width shrinks we must remove all components with relative * width from the DOM, update their width when they do not affect the * available space and finally restore them to the original state */ if (shrinking) { updateRelativeSizedComponents(true, false); } } private void updateRelativeSizedComponents(boolean relativeWidth, boolean relativeHeight) { Set<Widget> relativeSizeWidgets = new HashSet<Widget>(); for (Widget widget : locationToWidget.values()) { FloatSize relativeSize = client.getRelativeSize(widget); if (relativeSize != null) { if ((relativeWidth && (relativeSize.getWidth() >= 0.0f)) || (relativeHeight && (relativeSize.getHeight() >= 0.0f))) { relativeSizeWidgets.add(widget); widget.getElement().getStyle().setProperty("position", "absolute"); } } } for (Widget widget : relativeSizeWidgets) { client.handleComponentRelativeSize(widget); widget.getElement().getStyle().setProperty("position", ""); } } /** * Compares newSize with currentSize and returns true if it is clear that * newSize is larger than currentSize. Returns false if newSize is smaller * or if it is unclear which one is smaller. * * @param newSize * @param currentSize * @return */ private boolean isLarger(String newSize, String currentSize) { if (newSize.equals("") || currentSize.equals("")) { return false; } if (!newSize.endsWith("px") || !currentSize.endsWith("px")) { return false; } int newSizePx = Integer.parseInt(newSize.substring(0, newSize.length() - 2)); int currentSizePx = Integer.parseInt(currentSize.substring(0, currentSize.length() - 2)); boolean larger = newSizePx > currentSizePx; return larger; } }