/*
* 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.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;
}
}