From: Risto Yrjänä Date: Mon, 29 Sep 2008 10:47:58 +0000 (+0000) Subject: Copied CoordinateLayout from incubator to trunk X-Git-Tag: 6.7.0.beta1~4065 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=094c2466aab4dd2b84980d4de559f3947caac264;p=vaadin-framework.git Copied CoordinateLayout from incubator to trunk svn changeset:5542/svn branch:trunk --- diff --git a/WebContent/ITMILL/themes/default/coordinatelayout/coordinatelayout.css b/WebContent/ITMILL/themes/default/coordinatelayout/coordinatelayout.css new file mode 100644 index 0000000000..f85c43a9d2 --- /dev/null +++ b/WebContent/ITMILL/themes/default/coordinatelayout/coordinatelayout.css @@ -0,0 +1,10 @@ +/* + * CoordinateLayout + */ + +.i-coordinatelayout-margin-values{ + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; +} \ No newline at end of file diff --git a/WebContent/ITMILL/themes/default/styles.css b/WebContent/ITMILL/themes/default/styles.css index 713150016b..06bdfbf14a 100644 --- a/WebContent/ITMILL/themes/default/styles.css +++ b/WebContent/ITMILL/themes/default/styles.css @@ -367,6 +367,16 @@ input.i-modified, font-size:xx-small; } +/* + * CoordinateLayout + */ + +.i-coordinatelayout-margin-values{ + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; +} .i-datefield-button { font-size:13px; width: 22px; diff --git a/src/com/itmill/toolkit/terminal/gwt/client/DefaultWidgetSet.java b/src/com/itmill/toolkit/terminal/gwt/client/DefaultWidgetSet.java index 7ee91ca5ce..560ea4524d 100644 --- a/src/com/itmill/toolkit/terminal/gwt/client/DefaultWidgetSet.java +++ b/src/com/itmill/toolkit/terminal/gwt/client/DefaultWidgetSet.java @@ -11,6 +11,7 @@ import com.google.gwt.user.client.ui.Widget; import com.itmill.toolkit.terminal.gwt.client.ui.IAccordion; import com.itmill.toolkit.terminal.gwt.client.ui.IButton; import com.itmill.toolkit.terminal.gwt.client.ui.ICheckBox; +import com.itmill.toolkit.terminal.gwt.client.ui.ICoordinateLayout; import com.itmill.toolkit.terminal.gwt.client.ui.ICustomComponent; import com.itmill.toolkit.terminal.gwt.client.ui.ICustomLayout; import com.itmill.toolkit.terminal.gwt.client.ui.IDateFieldCalendar; @@ -189,6 +190,9 @@ public class DefaultWidgetSet implements WidgetSet { } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IPopupView" .equals(className)) { return new IPopupView(); + } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ICoordinateLayout" + .equals(className)) { + return new ICoordinateLayout(); } return new IUnknownComponent(); @@ -311,6 +315,8 @@ public class DefaultWidgetSet implements WidgetSet { return "com.itmill.toolkit.terminal.gwt.client.ui.IMenuBar"; } else if ("popupview".equals(tag)) { return "com.itmill.toolkit.terminal.gwt.client.ui.IPopupView"; + } else if ("coordinatelayout".equals(tag)) { + return "com.itmill.toolkit.terminal.gwt.client.ui.ICoordinateLayout"; } return "com.itmill.toolkit.terminal.gwt.client.ui.IUnknownComponent"; diff --git a/src/com/itmill/toolkit/terminal/gwt/client/ui/ICoordinateLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/ui/ICoordinateLayout.java new file mode 100644 index 0000000000..93b0fcbf0e --- /dev/null +++ b/src/com/itmill/toolkit/terminal/gwt/client/ui/ICoordinateLayout.java @@ -0,0 +1,1165 @@ +package com.itmill.toolkit.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import com.google.gwt.core.client.GWT; +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.HTML; +import com.google.gwt.user.client.ui.Widget; +import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection; +import com.itmill.toolkit.terminal.gwt.client.BrowserInfo; +import com.itmill.toolkit.terminal.gwt.client.Container; +import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener; +import com.itmill.toolkit.terminal.gwt.client.ICaption; +import com.itmill.toolkit.terminal.gwt.client.Paintable; +import com.itmill.toolkit.terminal.gwt.client.UIDL; +import com.itmill.toolkit.terminal.gwt.client.Util; +import com.itmill.toolkit.terminal.gwt.client.Util.WidgetSpaceAllocator; + +public class ICoordinateLayout extends ComplexPanel implements Container, + ContainerResizedListener, WidgetSpaceAllocator { + + /** Class name, prefix in styling */ + public static final String CLASSNAME = "i-coordinatelayout"; + + /** CSS class identifier for margin values */ + public static final String CSSID = "margin-values"; + + /** For server-client communication */ + protected String uidlId; + protected ApplicationConnection client; + + /** For inner classes */ + protected final ICoordinateLayout hostReference = this; + + /** Indexes to coordinate arrays */ + protected static final int LEFT = 0; + protected static final int TOP = 1; + protected static final int WIDTH = 2; + protected static final int HEIGHT = 3; + protected static final int RIGHT = 4; + protected static final int BOTTOM = 5; + + /** Data structures for components */ + // current components, including captions + protected final ArrayList componentList; + + // component -> caption mappers + protected final HashMap componentToHeader; + protected final HashMap componentToMarker; + + // component -> properties mappers + protected final HashMap componentToCoords; + protected final HashMap componentToZ; + protected final HashMap componentToArea; + + // components to draw on the next layout update + protected ArrayList toUpdate; + + // current layout margins and width/height + protected int[] layout = new int[6]; + + protected MarginInfo marginInfo; // from UIDL + protected int[] margins; // from CSS + + public ICoordinateLayout() { + super(); + setElement(DOM.createDiv()); + + // Set style attributes + setStyleName(CLASSNAME); + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(getElement(), "zoom", "1"); + } + + marginInfo = new MarginInfo(0); + + // Init data structures + componentList = new ArrayList(); + toUpdate = new ArrayList(); + componentToCoords = new HashMap(); + componentToHeader = new HashMap(); + componentToMarker = new HashMap(); + componentToZ = new HashMap(); + componentToArea = new HashMap(); + + } + + /** + * This enables us to read the margins when the layout is actually attached + * to the DOM-tree. + * + * @see com.google.gwt.user.client.ui.Panel#onLoad() + */ + protected void onLoad() { + margins = readMarginsFromCSS(); + super.onLoad(); + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.Paintable#updateFromUIDL(com.itmill + * .toolkit.terminal.gwt.client.UIDL, + * com.itmill.toolkit.terminal.gwt.client.ApplicationConnection) + */ + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // This call should be made first. Ensure correct implementation, + // and don't let the containing coordinateLayout manage caption, etc. + + if (client.updateComponent(this, uidl, false)) { + return; + } + + // These are for future server connections + this.client = client; + + // Enable / disable margins + if (uidl.hasAttribute("margins")) { + marginInfo = new MarginInfo(uidl.getIntAttribute("margins")); + } else { + marginInfo = new MarginInfo(0); + } + + // Start going through the component tree + UIDL componentsFromUIDL = uidl; + ArrayList newComponents = new ArrayList(); + ArrayList removedComponents = new ArrayList(); + int zIndex = 0; + + for (Iterator componentIterator = componentsFromUIDL + .getChildIterator(); componentIterator.hasNext();) { + + // Extract the values from the UIDL + final UIDL componentUIDL = componentIterator.next(); + final UIDL componentDataUIDL = componentUIDL.getChildUIDL(0); + final Paintable componentDataPaintable = client + .getPaintable(componentDataUIDL); + final Widget componentWidget = (Widget) componentDataPaintable; + final String coordString = componentUIDL + .getStringAttribute("position"); + + if (!componentList.contains(componentWidget)) { // Initial draw + add(componentWidget); + componentDataPaintable + .updateFromUIDL(componentDataUIDL, client); + + } else { // normal update + componentDataPaintable + .updateFromUIDL(componentDataUIDL, client); + } + + // Save the coordinate string from the UIDL + if (!coordString.equals(componentToCoords.get(componentWidget))) { + toUpdate.add(componentWidget); + } + componentToCoords.put(componentWidget, coordString); + + // Components come from the server in the correct order, client just + // needs to set the z-index + updateZ(componentWidget, zIndex++); + + // Add the component to the list. This list is later checked against + // the list of current components + newComponents.add(componentWidget); + + }// for + + // Clean children that don't exist in the current UIDL + // (except for captions) + for (Iterator iterator = componentList.iterator(); iterator + .hasNext();) { + Widget w = iterator.next(); + if (!newComponents.contains(w) && !(w instanceof CustomCaption)) { + removedComponents.add(w); + } + } + + while (!removedComponents.isEmpty()) { + removePaintable((Paintable) removedComponents.get(0)); + removedComponents.remove(0); + } + + componentList.clear(); + componentList.addAll(newComponents); + + // Make sure coordinateLayout gets done every time + iLayout(-1, -1); + + }// updateFromUIDL + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener#iLayout + * (int, int) + */ + public void iLayout(int availableWidth, int availableHeight) { + // shake + // TODO is this necessary? + getOffsetWidth(); + getOffsetHeight(); + + // Get our current area and check if it has changed + // If it has, we need to recalculate all + int[] newSize = getDimensionsArray(); + if ((newSize[WIDTH] != layout[WIDTH]) + || (newSize[HEIGHT] != layout[HEIGHT])) { + toUpdate.clear(); + toUpdate = getDrawableComponents(); + layout = newSize; + } + + if (!toUpdate.isEmpty()) { + + // Run layout functions for children + Util.runDescendentsLayout(this); + + // Go over all children and calculate their positions + for (Iterator componentIterator = toUpdate.iterator(); componentIterator + .hasNext();) { + + Widget componentWidget = componentIterator.next(); + CustomCaption componentHeader = componentToHeader + .get((Paintable) componentWidget); + CustomCaption componentMarker = componentToMarker + .get((Paintable) componentWidget); + + // Reset position info + resetPositionAttributes(componentWidget); + maximizeArea(componentWidget); + + // Calculate real pixel values from the coordinate string + int[] coords = parseCoordString(componentToCoords + .get(componentWidget)); + + // Determine which sides of the component must be attached + // ( == sides with a determined position) + boolean[] sidesToAttach = new boolean[6]; + for (int i = 0; i < coords.length; i++) { + sidesToAttach[i] = (coords[i] != -1); + } + + // At least one of (TOP, BOTTOM) must be attached to ensure the + // captions are displayed properly + if (!sidesToAttach[TOP] && !sidesToAttach[BOTTOM]) { + sidesToAttach[TOP] = true; + } + // At least one of (LEFT, RIGHT) must be attached to ensure the + // captions are displayed properly + if (!sidesToAttach[LEFT] && !sidesToAttach[RIGHT]) { + sidesToAttach[LEFT] = true; + } + + // If width or height is not given, try to calculate from other + // values + if (coords[WIDTH] == -1) { + coords[WIDTH] = layout[WIDTH]; + + if (coords[LEFT] != -1) { + coords[WIDTH] -= coords[LEFT]; + } + if (coords[RIGHT] != -1) { + coords[WIDTH] -= coords[RIGHT]; + } + + } + + if (coords[HEIGHT] == -1) { + coords[HEIGHT] = layout[HEIGHT]; + + if (coords[TOP] != -1) { + coords[HEIGHT] -= coords[TOP]; + } + if (coords[BOTTOM] != -1) { + coords[HEIGHT] -= coords[BOTTOM]; + } + } + + // Sanity check for width & height + if (coords[HEIGHT] > layout[HEIGHT]) { + coords[HEIGHT] = layout[HEIGHT]; + } else if (coords[HEIGHT] < 0) { + coords[HEIGHT] = 0; + } + + if (coords[WIDTH] > layout[WIDTH]) { + coords[WIDTH] = layout[WIDTH]; + } else if (coords[WIDTH] < 0) { + coords[WIDTH] = 0; + } + + // Force width and height on margins so that they + // have some effect + calculateMargins(coords); + + // Sanity checks. Some browsers render incorrectly when for + // example relative[LEFT] + relative[RIGHT] > layout[WIDTH] + for (int i = 0; i < coords.length; i++) { + if (coords[i] < 0) { + coords[i] = 0; + } + } + + // height overflows + if ((coords[TOP] + coords[BOTTOM]) > getOffsetHeight()) { + if (coords[TOP] > coords[BOTTOM]) { + coords[TOP] -= coords[TOP] - coords[BOTTOM]; + } else { + coords[BOTTOM] -= coords[BOTTOM] - coords[TOP]; + } + } + + // width overflows + if (coords[RIGHT] + coords[LEFT] > getOffsetWidth()) { + if (coords[RIGHT] > coords[LEFT]) { + coords[RIGHT] -= coords[RIGHT] - coords[LEFT]; + } else { + coords[LEFT] -= coords[LEFT] - coords[RIGHT]; + } + } + + // this call puts the component to the componentToPosition + // map and sets the style attributes + setWidgetPosition(componentWidget, coords, sidesToAttach); + + // Update caption position + // Header has icon and caption text + if (componentHeader != null) { + updateCaptionPosition(componentHeader, componentWidget); + } + // Marker has error and required + if (componentMarker != null) { + updateCaptionPosition(componentMarker, componentWidget); + } + } + } + + toUpdate.clear(); + } + + /** + * Set positional attributes for given widget + * + * @param w + * @param newCoords + * @param sidesToAttach + */ + public void setWidgetPosition(Widget w, int[] newCoords, + boolean[] sidesToAttach) { + + resetPositionAttributes(w); + Element widgetElement = w.getElement(); + Element areaElement; + CustomCaption componentHeader = componentToHeader.get((Paintable) w); + CustomCaption componentMarker = componentToMarker.get((Paintable) w); + + // Attach the widget to the sides of the surrounding area element + if (sidesToAttach[TOP] || sidesToAttach[HEIGHT]) { + int margin = 0; + if (componentHeader != null) { + margin += componentHeader.getOffsetHeight(); + } + DOM.setStyleAttribute(widgetElement, "top", Integer + .toString(margin) + + "px"); + } + if (sidesToAttach[RIGHT] || sidesToAttach[WIDTH]) { + int margin = 0; + if (componentMarker != null) { + margin += componentMarker.getOffsetWidth(); + } + DOM.setStyleAttribute(widgetElement, "right", Integer + .toString(margin) + + "px"); + } + if (sidesToAttach[BOTTOM] || sidesToAttach[HEIGHT]) { + DOM.setStyleAttribute(widgetElement, "bottom", "0px"); + } + if (sidesToAttach[LEFT] || sidesToAttach[WIDTH]) { + DOM.setStyleAttribute(widgetElement, "left", "0px"); + } + + // Create / get the area element for this component + if (componentToArea.get(w) == null) { + areaElement = DOM.createDiv(); + // Element tree should be layoutElement -> areaElement -> + // componentElement + DOM.appendChild(getElement(), areaElement); + DOM.removeChild(getElement(), widgetElement); + DOM.appendChild(areaElement, widgetElement); + + DOM.setStyleAttribute(areaElement, "position", "absolute"); + DOM.setStyleAttribute(areaElement, "overflow", "hidden"); + + // Component growth areas are hidden so that they don't interfere + // with other components + DOM.setStyleAttribute(areaElement, "visibility", "hidden"); + + componentToArea.put(w, areaElement); + } else { + areaElement = componentToArea.get(w); + } + + // set the margin according to the values given + DOM.setStyleAttribute(areaElement, "margin", (newCoords[TOP] + "px " + + newCoords[RIGHT] + "px " + newCoords[BOTTOM] + "px " + + newCoords[LEFT] + "px")); + DOM.setStyleAttribute(areaElement, "width", newCoords[WIDTH] + "px"); + DOM.setStyleAttribute(areaElement, "height", newCoords[HEIGHT] + "px"); + + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.Container#updateCaption(com.itmill + * .toolkit.terminal.gwt.client.Paintable, + * com.itmill.toolkit.terminal.gwt.client.UIDL) + */ + public void updateCaption(Paintable component, UIDL uidl) { + CustomCaption header = componentToHeader.get(component); + CustomCaption marker = componentToMarker.get(component); + + if (ICaption.isNeeded(uidl)) { + boolean headerNeeded = uidl.getStringAttribute("caption") != null + || uidl.hasAttribute("icon"); + + boolean markerNeeded = uidl.hasAttribute("error") + || uidl.hasAttribute("required"); + + // if we have both, we display them on the header + if (headerNeeded && markerNeeded) { + // header might be in wrong format (separate marker & header) + if (header != null + && !header.isMode(CustomCaption.COMBINED_MODE)) { + componentToHeader.remove(component); + remove(header); + header = null; + } + // header might not exist + if (header == null) { + header = new CustomCaption( + component, + client, + (CustomCaption.HEADER_MODE | CustomCaption.MARKER_MODE)); + add(header); + componentToHeader.put(component, header); + } + + // update + header.updateCaption(uidl); + updateCaptionPosition(header, (Widget) component); + + // if we have both we don't need a separate marker + if (marker != null) { + componentToMarker.remove(component); + remove(marker); + } + + } + // if we only need a header + else if (headerNeeded) { + + if (header == null) { + + header = new CustomCaption(component, client, + CustomCaption.HEADER_MODE); + add(header); + componentToHeader.put(component, header); + + } + + header.updateCaption(uidl); + + // caption position + updateCaptionPosition(header, (Widget) component); + + } + + // if we only need a marker + else if (markerNeeded) { + + if (marker == null) { + + marker = new CustomCaption(component, client, + CustomCaption.MARKER_MODE); + add(marker); + componentToMarker.put(component, marker); + + } + + marker.updateCaption(uidl); + + // caption position + updateCaptionPosition(marker, (Widget) component); + } + } else { // No caption + if (header != null) { + remove(header); + componentToHeader.remove(component); + } else if (marker != null) { + remove(marker); + componentToMarker.remove(component); + } + } + + } + + /** + * Update caption position according to the parent component position. + * + * @param c + * @param parent + */ + public void updateCaptionPosition(CustomCaption c, Widget parent) { + + int parentTop, parentRight, parentBottom, parentLeft, parentHeight, parentWidth; + + Element captionElement = c.getElement(); + Element parentElement = parent.getElement(); + Element areaElement = componentToArea.get(parent); + + if (areaElement != null + && !DOM.isOrHasChild(areaElement, c.getElement())) { + DOM.removeChild(getElement(), c.getElement()); + DOM.appendChild(areaElement, c.getElement()); + } + + resetPositionAttributes((Widget) c); + + parentTop = getPositionValue(parentElement, "top"); + parentRight = getPositionValue(parentElement, "right"); + parentBottom = getPositionValue(parentElement, "bottom"); + parentLeft = getPositionValue(parentElement, "left"); + parentHeight = parent.getOffsetHeight(); + parentWidth = parent.getOffsetWidth(); + + if (c.isMode(CustomCaption.HEADER_MODE)) { + if (parentBottom == -1 || parentTop != -1) { + DOM.setStyleAttribute(captionElement, "top", "0px"); + } else { + DOM.setStyleAttribute(captionElement, "bottom", parentHeight + + "px"); + } + + if (parentRight == -1 || parentLeft != -1) { + DOM.setStyleAttribute(captionElement, "left", "0px"); + } else { + int right = parentRight + parentWidth - c.getOffsetWidth(); + DOM.setStyleAttribute(captionElement, "right", right + "px"); + } + } else if (c.isMode(CustomCaption.MARKER_MODE)) { + if (parentBottom == -1 || parentTop != -1) { + DOM.setStyleAttribute(captionElement, "top", parentTop + "px"); + } else { + int bottom = parentBottom + parentHeight - c.getOffsetHeight(); + DOM.setStyleAttribute(captionElement, "bottom", bottom + "px"); + } + + if (parentRight == -1 || parentLeft != -1) { + DOM.setStyleAttribute(captionElement, "left", parentWidth + + "px"); + } else { + int right = parentRight - c.getOffsetWidth(); + DOM.setStyleAttribute(captionElement, "right", right + "px"); + } + } + + } + + /** + * Update the z-index of a component + the z-index of the caption + * + * @param component + * @param zIndex + */ + public void updateZ(Widget component, int zIndex) { + + DOM.setStyleAttribute(component.getElement(), "zIndex", "" + + String.valueOf(zIndex)); + componentToZ.put(component, new Integer(zIndex)); + + // Set caption z-index (same as parent components z) + if (componentToHeader.get((Paintable) component) != null) { + CustomCaption h = componentToHeader.get((Paintable) component); + DOM.setStyleAttribute(h.getElement(), "zIndex", "" + zIndex); + } + if (componentToMarker.get((Paintable) component) != null) { + CustomCaption m = componentToMarker.get((Paintable) component); + DOM.setStyleAttribute(m.getElement(), "zIndex", "" + zIndex); + } + } + + /** + * Remove & unregister a paintable component + * + * @param p + * @return + */ + public boolean removePaintable(Paintable p) { + CustomCaption header = componentToHeader.get(p); + CustomCaption marker = componentToMarker.get(p); + if (header != null) { + componentToHeader.remove(p); + remove(header); + } + if (marker != null) { + componentToHeader.remove(p); + remove(marker); + } + client.unregisterPaintable(p); + return remove((Widget) p); + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.Container#childComponentSizesUpdated + * () + */ + public boolean childComponentSizesUpdated() { + return false; + } + + /** + * Remove all position-related attributes from a given element + * + * @param e + */ + protected void resetPositionAttributes(Widget w) { + Element e = w.getElement(); + DOM.setStyleAttribute(e, "top", ""); + DOM.setStyleAttribute(e, "right", ""); + DOM.setStyleAttribute(e, "bottom", ""); + DOM.setStyleAttribute(e, "left", ""); + DOM.setStyleAttribute(e, "width", ""); + DOM.setStyleAttribute(e, "height", ""); + } + + /** + * Calculates percentage from value if needed + * + * @param coordString + * @return + */ + protected int[] parseCoordString(String coordString) { + int[] relative = new int[6]; + boolean[] isPercent = new boolean[6]; + String[] values = coordString.split(","); + + for (int i = 0; i < values.length; i++) { + + // string parsing + if (values[i].startsWith("-")) { // anything with '-' equals -1 + relative[i] = -1; + } else { + if (values[i].endsWith("%")) { + isPercent[i] = true; + relative[i] = Integer.parseInt(values[i].substring(0, + values[i].length() - 1)); // without '%' + } else { + relative[i] = Integer.parseInt(values[i]); + } + + } + + // relative calculations + if (isPercent[i]) { + float multiplier = (float) relative[i] / 100; + + if (i == LEFT || i == RIGHT || i == WIDTH) { + float width = (float) layout[WIDTH]; + relative[i] = (int) (width * multiplier); + } else { + float height = (float) layout[HEIGHT]; + relative[i] = (int) (height * multiplier); + } + } + } + return relative; + } + + /** + * Get the dimensions for the layout, including margins. + * + * @return + */ + protected int[] getDimensionsArray() { + int[] newSize = new int[6]; + + newSize[LEFT] = marginInfo.hasLeft() ? margins[3] : 0; + newSize[TOP] = marginInfo.hasTop() ? margins[0] : 0; + newSize[BOTTOM] = marginInfo.hasBottom() ? margins[2] : 0; + newSize[RIGHT] = marginInfo.hasRight() ? margins[1] : 0; + newSize[WIDTH] = this.getOffsetWidth() - newSize[LEFT] - newSize[RIGHT]; + newSize[HEIGHT] = this.getOffsetHeight() - newSize[TOP] + - newSize[BOTTOM]; + + return newSize; + } + + /* + * This is called to "reset" area information so we can calculate component + * size correctly. The layout must have correct dimensions when this is + * called. + */ + protected void maximizeArea(Widget component) { + if (componentToArea.get(component) != null) { + Element areaElement = componentToArea.get(component); + DOM.setStyleAttribute(areaElement, "margin", "0"); + DOM.setStyleAttribute(areaElement, "width", layout[WIDTH] + "px"); + DOM.setStyleAttribute(areaElement, "height", layout[HEIGHT] + "px"); + } + } + + /* + * This method calculates the final coordinates for a given component. It + * "fills in" the gaps from the given coordinates. + */ + protected void calculateMargins(int[] coords) { + int left, top, right, bottom; + + // left + if (coords[LEFT] == -1 & coords[RIGHT] == -1) { + left = layout[LEFT]; + } else if (coords[LEFT] == -1) { + left = layout[WIDTH] - coords[RIGHT] - coords[WIDTH] + layout[LEFT]; + + } else { + left = coords[LEFT] + layout[LEFT]; + } + + // right + if (coords[RIGHT] == -1 & coords[LEFT] == -1) { + right = layout[RIGHT]; + } else if (coords[RIGHT] == -1) { + right = layout[WIDTH] - coords[LEFT] - coords[WIDTH] + + layout[RIGHT]; + } else { + right = coords[RIGHT] + layout[RIGHT]; + } + + // top + if (coords[TOP] == -1 & coords[BOTTOM] == -1) { + top = layout[TOP]; + } else if (coords[TOP] == -1) { + top = layout[HEIGHT] - coords[HEIGHT] - coords[BOTTOM] + + layout[TOP]; + } else { + top = coords[TOP] + layout[TOP]; + } + + // bottom + if (coords[BOTTOM] == -1 & coords[TOP] == -1) { + bottom = layout[BOTTOM]; + } else if (coords[BOTTOM] == -1) { + bottom = layout[HEIGHT] - coords[TOP] - coords[HEIGHT] + + layout[BOTTOM]; + } else { + bottom = coords[BOTTOM] + layout[BOTTOM]; + } + + coords[LEFT] = left; + coords[TOP] = top; + coords[RIGHT] = right; + coords[BOTTOM] = bottom; + + } + + /* + * Used to retrieve e.g. the "top" and "right" values for an element + */ + protected int getPositionValue(Element e, String position) { + String value = DOM.getStyleAttribute(e, position); + if (!"".equals(value)) { + return parsePixel(value); + } else { + return -1; + } + } + + /* Read margin info from CSS */ + protected int[] readMarginsFromCSS() { + int[] margins = new int[4]; + String style = CLASSNAME + "-" + CSSID; + String[] cssProperties = new String[] { "margin-top", "margin-right", + "margin-bottom", "margin-left" }; + + String cssValue = ""; + String[] cssValues = new String[cssProperties.length]; + + Element e = DOM.createDiv(); + DOM.appendChild(this.getElement(), e); + DOM.setStyleAttribute(e, "position", "absolute"); + DOM.setStyleAttribute(e, "height", "100px"); + DOM.setStyleAttribute(e, "width", "100px"); + DOM.setElementProperty(e, "className", style); + DOM.setInnerHTML(e, "."); + + try { + if (BrowserInfo.get().isIE()) { + cssValue = getIEMargin(e); + + cssValues = cssValue.split("\\s"); + + switch (cssValues.length) { + + case 4: + for (int i = 0; i < 4; i++) { + margins[i] = parsePixel(cssValues[i]); + } + break; + + case 3: + margins[0] = parsePixel(cssValues[0]); + margins[1] = margins[3] = parsePixel(cssValues[1]); + margins[2] = parsePixel(cssValues[2]); + break; + + case 2: + margins[0] = margins[2] = parsePixel(cssValues[0]); + margins[1] = margins[3] = parsePixel(cssValues[1]); + break; + + case 1: + int intValue = parsePixel(cssValues[0]); + for (int i = 0; i < margins.length; i++) { + margins[i] = intValue; + } + } + } else { + for (int i = 0; i < cssValues.length; i++) { + cssValues[i] = getMargin(e, cssProperties[i]); + margins[i] = parsePixel(cssValues[i]); + } + } + + } catch (Exception exc) { + // Error in CSS margin format or missing value, defaulting all to 0 + GWT.log("Error reading CSS", exc); + for (int i = 0; i < margins.length; i++) { + margins[i] = 0; + } + } + DOM.removeChild(this.getElement(), e); + return margins; + } + + /* "10px" => 10 */ + protected int parsePixel(String str) throws NumberFormatException { + return Integer.parseInt(str.substring(0, str.length() - 2)); + } + + /** + * Retrieve margin for other browsers + * + * @param e + * @param CSSProp + * @return + */ + protected native String getMargin(Element e, String CSSProp)/*-{ + return $wnd.getComputedStyle(e,null).getPropertyValue(CSSProp); + }-*/; + + /** + * Retrieves margin info in IE + * + * @param e + * @return + */ + protected native String getIEMargin(Element e)/*-{ + return e.currentStyle.margin; + }-*/; + + /** + * @return all components that are not captions + */ + protected ArrayList getDrawableComponents() { + ArrayList list = new ArrayList(); + for (Widget w : componentList) { + if (!(w instanceof CustomCaption)) { + list.add(w); + } + } + return list; + } + + /* + * Widget methods + */ + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.ComplexPanel#iterator() + */ + public Iterator iterator() { + return componentList.iterator(); + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.Container#replaceChildComponent + * (com.google.gwt.user.client.ui.Widget, + * com.google.gwt.user.client.ui.Widget) + */ + public void replaceChildComponent(Widget from, Widget to) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.terminal.gwt.client.Container#hasChildComponent(com + * .google.gwt.user.client.ui.Widget) + */ + public boolean hasChildComponent(Widget w) { + return componentList.contains(w); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Panel#add(com.google.gwt.user.client.ui + * .Widget) + */ + public void add(Widget w) { + if (!componentList.contains(w)) { + w.removeFromParent(); + } + + // these attributes are common to all elements + DOM.setStyleAttribute(w.getElement(), "position", "absolute"); + DOM.setStyleAttribute(w.getElement(), "overflow", "hidden"); + DOM.setStyleAttribute(w.getElement(), "visibility", "visible"); + componentList.add(w); + + super.add(w, getElement()); + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.ComplexPanel#remove(com.google.gwt.user + * .client.ui.Widget) + */ + public boolean remove(Widget w) { + boolean wasRemoved = super.remove(w); + if (wasRemoved) { + resetPositionAttributes(w); + + // If removing a component, remove its area + if (!(w instanceof CustomCaption)) { + DOM.removeChild(this.getElement(), componentToArea.get(w)); + } + + componentList.remove(w); + componentToZ.remove(w); + componentToArea.remove(w); + } + return wasRemoved; + } + + public int getAllocatedHeight(Widget child) { + Element area = componentToArea.get(child); + if (area != null) { + return area.getOffsetHeight(); + } else { + return layout[HEIGHT]; + } + } + + public int getAllocatedWidth(Widget child) { + Element area = componentToArea.get(child); + if (area != null) { + return area.getOffsetWidth(); + } else { + return layout[WIDTH]; + } + } + + /** + * Custom caption class to encapsulate different caption parts + */ + public class CustomCaption extends HTML { + public static final String CLASSNAME = "i-caption"; + + // Bit masks for different modes + public static final int HEADER_MODE = 1; + public static final int MARKER_MODE = 2; + public static final int COMBINED_MODE = HEADER_MODE | MARKER_MODE; + + private final Paintable owner; + + private Element errorIndicatorElement; + + private Element requiredFieldIndicator; + + private Icon icon; + + private Element captionText; + + private final ApplicationConnection client; + + private int mode; + + /** + * + * @param component + * optional owner of caption. If not set, getOwner will + * return null + * @param client + */ + public CustomCaption(Paintable component, ApplicationConnection client, + int mode) { + super(); + this.client = client; + owner = component; + setStyleName(CLASSNAME); + this.mode = mode; + } + + public void updateCaption(UIDL uidl) { + setVisible(!uidl.getBooleanAttribute("invisible")); + + setStyleName(getElement(), "i-disabled", uidl + .hasAttribute("disabled")); + + boolean isEmpty = true; + + if (uidl.hasAttribute("description")) { + if (captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + } + + if (isMode(HEADER_MODE)) { + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + + DOM.insertChild(getElement(), icon.getElement(), 0); + } + icon.setUri(uidl.getStringAttribute("icon")); + isEmpty = false; + } else { + if (icon != null) { + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + } + + if (uidl.hasAttribute("caption")) { + if (captionText == null) { + captionText = DOM.createSpan(); + DOM.insertChild(getElement(), captionText, + icon == null ? 0 : 1); + } + String c = uidl.getStringAttribute("caption"); + if (c == null) { + c = ""; + } else { + isEmpty = false; + } + DOM.setInnerText(captionText, c); + } else { + // TODO should span also be removed + } + } + + if (isMode(MARKER_MODE)) { + if (uidl.getBooleanAttribute("required")) { + isEmpty = false; + if (requiredFieldIndicator == null) { + requiredFieldIndicator = DOM.createSpan(); + DOM.setInnerText(requiredFieldIndicator, "*"); + DOM.setElementProperty(requiredFieldIndicator, + "className", "i-required-field-indicator"); + DOM.appendChild(getElement(), requiredFieldIndicator); + } + } else { + if (requiredFieldIndicator != null) { + DOM.removeChild(getElement(), requiredFieldIndicator); + requiredFieldIndicator = null; + } + } + + if (uidl.hasAttribute("error")) { + isEmpty = false; + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, + "className", "i-errorindicator"); + DOM.appendChild(getElement(), errorIndicatorElement); + } + } else if (errorIndicatorElement != null) { + DOM.removeChild(getElement(), errorIndicatorElement); + errorIndicatorElement = null; + } + } + // Workaround for IE weirdness, sometimes returns bad height in some + // circumstances when Caption is empty. See #1444 + // IE7 bugs more often. I wonder what happens when IE8 arrives... + if (BrowserInfo.get().isIE()) { + if (isEmpty) { + setHeight("0px"); + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + } else { + setHeight(""); + DOM.setStyleAttribute(getElement(), "overflow", ""); + } + + } + + } + + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + final Element target = DOM.eventGetTarget(event); + if (client != null && !(target == getElement())) { + client.handleTooltipEvent(event, owner); + } + } + + /** + * Returns Paintable for which this Caption belongs to. + * + * @return owner Widget + */ + public Paintable getOwner() { + return owner; + } + + /** + * Test if this CustomCaption is in a certain mode + * + * @param value + * @return + */ + public boolean isMode(int value) { + return (mode & value) == value; + } + } + +}// class ICoordinateLayout diff --git a/src/com/itmill/toolkit/tests/tickets/Ticket1267.java b/src/com/itmill/toolkit/tests/tickets/Ticket1267.java new file mode 100644 index 0000000000..4c022b7155 --- /dev/null +++ b/src/com/itmill/toolkit/tests/tickets/Ticket1267.java @@ -0,0 +1,477 @@ +package com.itmill.toolkit.tests.tickets; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.data.Property.ValueChangeEvent; +import com.itmill.toolkit.data.Property.ValueChangeListener; +import com.itmill.toolkit.data.util.HierarchicalContainer; +import com.itmill.toolkit.data.util.IndexedContainer; +import com.itmill.toolkit.data.validator.StringLengthValidator; +import com.itmill.toolkit.terminal.ThemeResource; +import com.itmill.toolkit.ui.Button; +import com.itmill.toolkit.ui.CheckBox; +import com.itmill.toolkit.ui.Component; +import com.itmill.toolkit.ui.CoordinateLayout; +import com.itmill.toolkit.ui.ExpandLayout; +import com.itmill.toolkit.ui.GridLayout; +import com.itmill.toolkit.ui.Label; +import com.itmill.toolkit.ui.MenuBar; +import com.itmill.toolkit.ui.OrderedLayout; +import com.itmill.toolkit.ui.Panel; +import com.itmill.toolkit.ui.Slider; +import com.itmill.toolkit.ui.Table; +import com.itmill.toolkit.ui.TextField; +import com.itmill.toolkit.ui.Tree; +import com.itmill.toolkit.ui.Window; +import com.itmill.toolkit.ui.CoordinateLayout.Coordinates; +import com.itmill.toolkit.ui.MenuBar.Command; +import com.itmill.toolkit.ui.MenuBar.MenuItem; +import com.itmill.toolkit.ui.Window.CloseEvent; +import com.itmill.toolkit.ui.Window.CloseListener; + +public class Ticket1267 extends Application { + + ArrayList componentList; + + CoordinateLayout coordinateLayout = new CoordinateLayout(); + Window main = new Window("Coordinatelayout demo"); + HashMap control = new HashMap(); + ExpandLayout mainLayout = new ExpandLayout(); + // Components + MenuBar mainMenu; + Window controlPanel = new Window("Control panel for components"); + + OrderedLayout ol = new OrderedLayout(); + + boolean defaultmargins = false; + + Application hostApp = this; + Class hostClass = Ticket1267.class; + + private int numberOfTables; + + private IndexedContainer emptyContainer = new IndexedContainer(); + private HierarchicalContainer emptyHierarchy = new HierarchicalContainer(); + + private int numberOfPanels; + + private int numberOfTextFields; + + private int numberOfTrees; + + public void init() { + + // Setup container + emptyContainer.addContainerProperty("First Name", String.class, null); + emptyContainer.addContainerProperty("Last Name", String.class, null); + emptyContainer.addContainerProperty("Year", Integer.class, null); + + // Setup menubar + mainMenu = getMainMenuBar(); + + // Setup control panel window + controlPanel.addListener(new CloseListener() { + + public void windowClose(CloseEvent e) { + main.removeWindow(e.getWindow()); + + } + + }); + + // TestField + TextField testField = new TextField("Pixel values"); + testField.setImmediate(true); + final Coordinates xy = new Coordinates("0,0"); + + testField.addListener(new ValueChangeListener() { + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.data.Property.ValueChangeListener#valueChange + * (com.itmill.toolkit.data.Property.ValueChangeEvent) + */ + public void valueChange(ValueChangeEvent event) { + String str = (String) event.getProperty().getValue(); + try { + xy.setCoordinates(str); + } catch (Exception e) { + main.showNotification("Wrong string format", e.toString(), + Window.Notification.TYPE_WARNING_MESSAGE); + return; + } + main.showNotification("Component added to " + xy.toString(), + Window.Notification.TYPE_TRAY_NOTIFICATION); + } + }); + + coordinateLayout.addComponent(testField, xy); + + // Setup coordinatelayout + coordinateLayout.setMargin(defaultmargins); + mainLayout.addComponent(mainMenu); + mainLayout.addComponent(coordinateLayout); + mainLayout.expand(coordinateLayout); + + main.setLayout(mainLayout); + + setMainWindow(main); + + } + + private MenuBar getMainMenuBar() { + MenuBar menu = new MenuBar(); + MenuItem add = menu.addItem("Add component", null); + MenuItem remove = menu.addItem("Remove component", null); + MenuItem modify = menu.addItem("Modify component", null); + MenuItem cPanel = menu.addItem("Show / hide control panel", null); + cPanel.setCommand(new Command() { + + public void menuSelected(MenuItem selectedItem) { + if (main.getChildWindows().contains(controlPanel)) { + main.removeWindow(controlPanel); + } else { + main.addWindow(controlPanel); + + } + } + }); + + final MenuItem margins = menu.addItem("Toggle margins", null); + margins.setCommand(new Command() { + + boolean marginsEnabled = defaultmargins; + + public void menuSelected(MenuItem selectedItem) { + if (marginsEnabled) { + margins.setIcon(null); + marginsEnabled = false; + coordinateLayout.setMargin(false); + } else { + margins.setIcon(new ThemeResource("icons/16/ok.png")); + marginsEnabled = true; + coordinateLayout.setMargin(true); + } + } + + }); + try { + add.addItem("Table", new AddCommand(hostClass + .getMethod("getNewTable"), remove, modify)); + + add.addItem("Panel", new AddCommand(hostClass + .getMethod("getNewPanel"), remove, modify)); + + add.addItem("TextField", new AddCommand(hostClass + .getMethod("getNewTextField"), remove, modify)); + add.addItem("Tree", new AddCommand(hostClass + .getMethod("getNewTree"), remove, modify)); + } catch (Exception e) { + e.printStackTrace(); + } + return menu; + } + + public Table getNewTable() { + Table table = new Table("Table " + numberOfTables++); + table.setContainerDataSource(emptyContainer); + table.setImmediate(true); + table.setIcon(new ThemeResource("icons/16/globe.png")); + + return table; + } + + public Panel getNewPanel() { + final Panel panel = new Panel("Panel " + numberOfPanels++); + Button addButton = new Button("Add component"); + addButton.addListener(new Button.ClickListener() { + int count = 0; + + public void buttonClick(Button.ClickEvent event) { + panel.addComponent(new Label("Component nro. " + count++)); + + } + }); + panel.addComponent(addButton); + + return panel; + } + + public TextField getNewTextField() { + TextField textField = new TextField( + "TextField " + numberOfTextFields++, + "This textfield has a validator"); + textField.setImmediate(true); + textField.setIcon(new ThemeResource("icons/16/document-image.png")); + textField.setRequired(true); + textField.addValidator(new StringLengthValidator( + "5 < String.length() < 10", 5, 10, false)); + + return textField; + } + + public Tree getNewTree() { + + Tree tree = new Tree("Tree " + numberOfTrees++); + + return tree; + } + + private class AddCommand implements Command { + + private Method addMethod; + private MenuItem removeMenu; + private MenuItem modifyMenu; + + public void menuSelected(MenuItem selectedItem) { + Component toAdd = null; + try { + toAdd = (Component) addMethod.invoke(hostApp); + } catch (Exception e) { + e.printStackTrace(); + } + String caption = toAdd.getCaption(); + Coordinates xy = coordinateLayout.addComponent(toAdd, "50%,50%"); + getMainWindow().showNotification( + "Component added to " + xy.toString(), + Window.Notification.TYPE_TRAY_NOTIFICATION); + + MenuItem modItem = getModMenuItem(modifyMenu, caption, toAdd); + removeMenu.addItem(caption, new RemoveCommand(toAdd, modItem, + modifyMenu)); + + controlPanel.addComponent(getControlPanel(toAdd)); + + } + + AddCommand(Method addMethod, MenuItem removeMenu, MenuItem modifyMenu) { + + this.addMethod = addMethod; + this.removeMenu = removeMenu; + this.modifyMenu = modifyMenu; + } + + } + + private class RemoveCommand implements Command { + + private Component toRemove; + private MenuItem modifyItem; + private MenuItem modifyMenu; + + public RemoveCommand(Component toRemove, MenuBar.MenuItem modifyItem, + MenuItem modifyMenu) { + this.toRemove = toRemove; + this.modifyItem = modifyItem; + this.modifyMenu = modifyMenu; + } + + public void menuSelected(MenuItem selectedItem) { + if (coordinateLayout.contains(toRemove)) { + coordinateLayout.removeComponent(toRemove); + MenuItem parent = selectedItem.getParent(); + parent.removeChild(selectedItem); + + if (modifyItem != null) { + modifyMenu.removeChild(modifyItem); + } + + controlPanel.removeComponent(getControlPanel(toRemove)); + control.remove(toRemove); + } + } + } + + public Panel getControlPanel(final Component c) { + + if (control.get(c) == null) { + Panel newPanel = new Panel("Controls for " + c.getCaption()); + newPanel.setLayout(new GridLayout(3, 6)); + + final int[] values = new int[6]; + Arrays.fill(values, -1); + + final CheckBox[] checkBoxArray = new CheckBox[6]; + final Slider[] sliderArray = new Slider[6]; + + String[] directions = { "Left", "Top", "Width", "Height", "Right", + "Bottom" }; + + final Label[] labelArray = new Label[6]; + + for (int i = 0; i < checkBoxArray.length; i++) { + final int j = i; + + checkBoxArray[i] = new CheckBox(directions[i]); + sliderArray[i] = new Slider(-1, 100, 1); + + labelArray[j] = new Label("-1"); + labelArray[j].setEnabled(false); + checkBoxArray[j].setImmediate(true); + sliderArray[j].setImmediate(true); + sliderArray[j].setEnabled(false); + sliderArray[j].setWidth("300px"); + + checkBoxArray[j].addListener(new ValueChangeListener() { + public void valueChange(ValueChangeEvent event) { + Boolean isChecked = (Boolean) checkBoxArray[j] + .getValue(); + if (isChecked.booleanValue()) { + sliderArray[j].setEnabled(true); + labelArray[j].setEnabled(true); + } else { + values[j] = -1; + labelArray[j].setValue(new Integer(values[j])); + sliderArray[j].setEnabled(false); + labelArray[j].setEnabled(false); + CoordinateLayout.Coordinates coords = coordinateLayout + .getCoordinates(c); + coords.setCoordinates(values[0], values[1], + values[2], values[3], values[4], values[5]); + coords.setUnitsPercent(true, true, true, true, + true, true); + + } + } + }); + + sliderArray[j].addListener(new ValueChangeListener() { + public void valueChange(ValueChangeEvent event) { + Double newValue = (Double) event.getProperty() + .getValue(); + values[j] = (int) newValue.doubleValue(); + CoordinateLayout.Coordinates coords = coordinateLayout + .getCoordinates(c); + coords.setCoordinates(values[0], values[1], values[2], + values[3], values[4], values[5]); + coords.setUnitsPercent(true, true, true, true, true, + true); + + labelArray[j].setValue(new Integer(values[j])); + + } + }); + } + + for (int i = 0; i < sliderArray.length; i++) { + newPanel.addComponent(checkBoxArray[i]); + newPanel.addComponent(sliderArray[i]); + newPanel.addComponent(labelArray[i]); + } + control.put(c, newPanel); + return newPanel; + } else { + return control.get(c); + } + } + + public MenuBar.MenuItem getModMenuItem(MenuItem parent, String id, + Component c) { + MenuBar.MenuItem item = null; + if (c instanceof Table) { + final Table table = (Table) c; + item = parent.addItem(id, null); + MenuBar.Command addContent = new MenuBar.Command() { + + public void menuSelected(MenuItem selectedItem) { + table + .addContainerProperty("First Name", String.class, + null); + table.addContainerProperty("Last Name", String.class, null); + table.addContainerProperty("Year", Integer.class, null); + + table.addItem(new Object[] { "Nicolaus", "Copernicus", + new Integer(1473) }, Integer.valueOf(1)); + table.addItem(new Object[] { "Tycho", "Brahe", + new Integer(1546) }, Integer.valueOf(2)); + table.addItem(new Object[] { "Giordano", "Bruno", + new Integer(1548) }, Integer.valueOf(3)); + table.addItem(new Object[] { "Galileo", "Galilei", + new Integer(1564) }, Integer.valueOf(4)); + table.addItem(new Object[] { "Johannes", "Kepler", + new Integer(1571) }, Integer.valueOf(5)); + table.addItem(new Object[] { "Isaac", "Newton", + new Integer(1643) }, Integer.valueOf(6)); + } + + }; + + MenuBar.Command removeContent = new MenuBar.Command() { + + public void menuSelected(MenuItem selectedItem) { + table.setContainerDataSource(emptyContainer); + + } + + }; + + item.addItem("Add content", addContent); + item.addItem("Remove content", removeContent); + + } else if (c instanceof Tree) { + final Tree tree = (Tree) c; + item = parent.addItem(id, null); + + MenuBar.Command addContent = new MenuBar.Command() { + public void menuSelected(MenuBar.MenuItem selectedItem) { + final Object[][] planets = new Object[][] { + new Object[] { "Mercury" }, + new Object[] { "Venus" }, + new Object[] { "Earth", "The Moon" }, + new Object[] { "Mars", "Phobos", "Deimos" }, + new Object[] { "Jupiter", "Io", "Europa", + "Ganymedes", "Callisto" }, + + new Object[] { "Saturn", "Titan", "Tethys", + "Dione", "Rhea", "Iapetus" }, + new Object[] { "Uranus", "Miranda", "Ariel", + "Umbriel", "Titania", "Oberon" }, + new Object[] { "Neptune", "Triton", "Proteus", + "Nereid", "Larissa" } }; + + /* Add planets as root items in the tree. */ + for (int i = 0; i < planets.length; i++) { + String planet = (String) (planets[i][0]); + tree.addItem(planet); + if (planets[i].length == 1) { + /* The planet has no moons so make it a leaf. */ + tree.setChildrenAllowed(planet, false); + } else { + /* Add children (moons) under the planets. */ + for (int j = 1; j < planets[i].length; j++) { + String moon = (String) planets[i][j]; + /* Add the item as a regular item. */ + tree.addItem(moon); + /* Set it to be a child. */ + tree.setParent(moon, planet); + /* Make the moons look like leaves. */ + tree.setChildrenAllowed(moon, false); + } + /* Expand the subtree. */ + tree.expandItemsRecursively(planet); + } + } + } + }; + + MenuBar.Command removeContent = new MenuBar.Command() { + + public void menuSelected(MenuBar.MenuItem selectedItem) { + tree.setContainerDataSource(emptyHierarchy); + } + + }; + + item.addItem("Add content", addContent); + item.addItem("Remove content", removeContent); + } + + return item; + } +} diff --git a/src/com/itmill/toolkit/ui/CoordinateLayout.java b/src/com/itmill/toolkit/ui/CoordinateLayout.java new file mode 100644 index 0000000000..87feebd57b --- /dev/null +++ b/src/com/itmill/toolkit/ui/CoordinateLayout.java @@ -0,0 +1,768 @@ +package com.itmill.toolkit.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +/** + * + * A layout that enables absolute positioning for its children. + * + * + */ +public class CoordinateLayout extends AbstractLayout { + + protected final ArrayList componentList; + protected final HashMap componentToCoord; + + public static final int LEFT = 0; + public static final int TOP = 1; + public static final int WIDTH = 2; + public static final int HEIGHT = 3; + public static final int RIGHT = 4; + public static final int BOTTOM = 5; + + /** + * Creates an empty coordinatelayout. The coordinateLayout is full size by + * default. + */ + public CoordinateLayout() { + super(); + componentList = new ArrayList(); + componentToCoord = new HashMap(); + + this.setSizeFull(); + } + + /** + * + *

+ * Adds a component to the layout at the specified coordinates. The values + * are treated as pixel values by default and for example + * {@link com.itmill.toolkit.ui.CoordinateLayout.Coordinates#setUnitPercent(int, boolean)} + * can be used to set the values as percentage. + *

+ * + *

+ * If the value is negative, it is interpreted as automatic (terminal + * decides). Null argument is not permitted and will throw an + * IllegalArgumentException. + *

+ *
+ *
Examples:
+ * + *
+ * addComponent(c, 0, 0, -1, -1, -1, -1) to attach to + * upper-left corner and let terminal decide size
+ * + *
Coordinates c = addComponent(c, 5, 5, 200, 200, -1, -1)
+ *
c.setUnitPercent(CoordinateLayout.LEFT, true)
+ *
c.setUnitPercent(CoordinateLayout.TOP, true) to attach 5% + * from upper-left corner and define size to 200 x 200 pixels
+ * + *
+ * addComponent(c, 0, 0, -1, -1, 0, 0) to stretch component to + * cover layout area (attached to all four corners)
+ *
+ * + * + * @param c + * component to be added + * @param left + * distance of component left from layout left + * @param top + * distance of component top from layout top + * @param width + * component width + * @param height + * component height + * @param right + * distance of component right from layout right + * @param bottom + * distance of component bottom from layout bottom + * @return the coordinates object for the component + * + * @throws IllegalArgumentException + */ + public Coordinates addComponent(Component c, int left, int top, int width, + int height, int right, int bottom) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException(); + } + + Coordinates newCoords = new Coordinates(left, top, width, height, + right, bottom); + componentToCoord.put(c, newCoords); + addComponent(c); + + return newCoords; + } + + /** + *

+ * Add a component to the specified coordinates. The string format is + * "left[%], top[%] [,width[%], height[%] [,right[%], bottom[%]]]". + * Null arguments will throw an IllegalArgumentException. + *

+ * + *
+ *
Examples for string format:
+ * + *
+ * addComponent(c, "0, 0") to attach to upper-left corner and + * let terminal decide size
+ * + *
+ * addComponent(c, "5%, 5%, 200, 200") to attach 5% from + * upper-left corner and define size to 200 x 200 pixels
+ * + *
+ * addComponent(c,"0, 0, -1, -1,0, 0") to stretch component to + * cover coordinateLayout area (attached to all four corners)
+ * + *
+ * + * @param c + * the component to be added + * @param coordString + * coordinates in the specified string format + * @return the created Coordinates object for the component + * + * @throws IllegalArgumentException + */ + public Coordinates addComponent(Component c, String coordString) + throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException(); + } + Coordinates newCoords = new Coordinates(coordString); + componentToCoord.put(c, newCoords); + addComponent(c); + + return newCoords; + } + + /** + * Adds a component to the layout at the specified coordinates. Null + * arguments are not permitted and will throw an IllegalArgumentException. + * + * @param c + * the component to be added + * @param coord + * the coordinates of the component as a Coordinates object + * @throws IllegalArgument + * Exception + */ + public void addComponent(Component c, CoordinateLayout.Coordinates coord) + throws IllegalArgumentException { + if (c == null || coord == null) { + throw new IllegalArgumentException(); + } + componentToCoord.put(c, coord); + addComponent(c); + } + + /** + * Sets the coordinates of a child component. Null arguments are not + * permitted and will throw an IllegalArgumentException. In addition, the + * user of this method must make sure that the given component is a child of + * this layout. + * + * @param c + * the target component + * @param left + * distance of component left from layout left + * @param top + * distance of component top from layout top + * @param width + * component width + * @param height + * component height + * @param right + * distance of component right from layout right + * @param bottom + * distance of component bottom from layout bottom + * @throws IllegalArgumentException + */ + public void setCoordinates(Component c, int left, int top, int width, + int height, int right, int bottom) throws IllegalArgumentException { + if (c == null || !componentList.contains(c)) { + throw new IllegalArgumentException(); + } + Coordinates coords = (Coordinates) componentToCoord.get(c); + coords.setCoordinates(left, top, width, height, right, bottom); + } + + /** + * Sets the coordinates of a child component. Null arguments are not + * permitted and will throw an IllegalArgumentException. In addition, the + * user of this method must make sure that the given component is a child of + * this coordinateLayout. + * + * @param c + * the component thats position is changed + * @param newCoord + * the new coordinates as a Coordinates object + * @throws IllegalArgumentException + */ + public void setCoordinates(Component c, + CoordinateLayout.Coordinates newCoord) + throws IllegalArgumentException { + if (c == null || newCoord == null || !componentList.contains(c)) { + throw new IllegalArgumentException(); + } + // Detach old coordinate object from this layout + Coordinates oldCoord = (Coordinates) componentToCoord.remove(c); + oldCoord.removeParentLayout(this); + + // Attach new + newCoord.addParentLayout(this); + componentToCoord.put(c, newCoord); + + requestRepaint(); + } + + /** + * This will put the given component at the top of the list. + * + * @param c + * the component to be put to the top + */ + public void sendToTop(Component c) { + componentList.remove(c); + componentList.add(c); + + requestRepaint(); + } + + /** + * This will put the given component at the bottom of the list. + * + * @param c + * the component to be put to the bottom + */ + public void sendToBottom(Component c) { + componentList.remove(c); + componentList.add(0, c); + + requestRepaint(); + } + + /** + * If a component is added with this method the component is added to the + * top-left corner of the layout. The coordinates can later be accessed via + * {@link CoordinateLayout#getCoordinates(Component)} + * + * @param c + * the component to be added + * @throws IllegalArgument + * Exception + * + * @see com.itmill.toolkit.ui.AbstractComponentContainer#addComponent(com.itmill.toolkit.ui.Component) + */ + public void addComponent(Component c) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException(); + } + componentList.add(c); + + // Someone might call this outside this object + if (componentToCoord.get(c) == null) { + componentToCoord.put(c, new Coordinates(0, 0, -1, -1, -1, -1)); + + } + + super.addComponent(c); + requestRepaint(); + ((Coordinates) componentToCoord.get(c)).addParentLayout(this); + } + + /** + * Removes the component from the coordinateLayout. Method will throw + * IllegalArgumentException if the given component is not a child of this + * layout. + * + * @param c + * the component to be removed + * @throws IllegalArgumentException + * + * @see com.itmill.toolkit.ui.AbstractComponentContainer#removeComponent(com.itmill.toolkit.ui.Component) + */ + public void removeComponent(Component c) throws IllegalArgumentException { + if (c == null || !componentList.contains(c)) { + throw new IllegalArgumentException(); + } + ((Coordinates) componentToCoord.get(c)).removeParentLayout(this); + + super.removeComponent(c); + componentList.remove(c); + componentToCoord.remove(c); + + requestRepaint(); + } + + /** + * Replaces a child component with a new one. The new component uses the + * same coordinates as the previous component. If the arguments are null or + * the component is not a child of this layout IllegalArgumentException is + * thrown. + * + * @param c1 + * the existing child component + * @param c2 + * the new component + * + * @throws IllegalArgumentException + * + * @see com.itmill.toolkit.ui.ComponentContainer#replaceComponent(com.itmill.toolkit.ui.Component, + * com.itmill.toolkit.ui.Component) + */ + public void replaceComponent(Component c1, Component c2) { + if (c1 == null || c2 == null || !componentList.contains(c1)) { + throw new IllegalArgumentException(); + } + componentList.set(componentList.indexOf(c1), c2); + requestRepaint(); + } + + /** + * Get the coordinates for a given component in this layout + * + * @param c + * @return the Coordinates object for the given component + * @throws IllegalArgumentException + * thrown if component is not in this coordinateLayout + */ + public Coordinates getCoordinates(Component c) + throws IllegalArgumentException { + if (c == null || componentToCoord.get(c) == null) { + throw new IllegalArgumentException(); + } + return (Coordinates) componentToCoord.get(c); + } + + /** + * @see com.itmill.toolkit.ui.ComponentContainer#getComponentIterator() + */ + public Iterator getComponentIterator() { + return componentList.iterator(); + } + + /** + * Returns the iterator for the coordinates of the child components in this + * layout. + * + * @return the iterator for the coordinate list + */ + public Iterator getComponentCoordinateIterator() { + return componentToCoord.values().iterator(); + } + + /** + * Check if this layout has components. + * + * @return true if this coordinateLayout has children, false otherwise + */ + public boolean hasComponents() { + return componentList.isEmpty(); + } + + /** + * Check if a given component is in this layout + * + * @param c + * @return true if the component is in this layout, false otherwise + */ + public boolean contains(Component c) { + return componentList.contains(c); + } + + /** + * Get child component by index. + * + * @param index + * must be index >= 0 and index < size() -1 + * @return the child component + * @throws IllegalArgumentException + * if given index is illegal + */ + public Component getComponentByIndex(int index) + throws IllegalArgumentException { + if (index < 0 || index > componentList.size() - 1) { + throw new IllegalArgumentException("Illegal index"); + } + + return (Component) componentList.get(index); + } + + /** + * Returns the number of child components. + * + * @return number of children + */ + public int size() { + return componentList.size(); + } + + /* + * (non-Javadoc) + * + * @see com.itmill.toolkit.ui.AbstractLayout#getTag() + */ + public String getTag() { + return "coordinatelayout"; + } + + /* + * (non-Javadoc) + * + * @see + * com.itmill.toolkit.ui.AbstractLayout#paintContent(com.itmill.toolkit. + * terminal.PaintTarget) + */ + public void paintContent(PaintTarget target) throws PaintException { + // Superclass writes any common attributes in the paint target. + super.paintContent(target); + + for (Iterator componentIterator = getComponentIterator(); componentIterator + .hasNext();) { + + Component component = (Component) componentIterator.next(); + Coordinates coords = (Coordinates) componentToCoord.get(component); + + target.startTag("component"); + target.addAttribute("position", coords.toString()); + + component.paint(target); + + target.endTag("component"); + + }// for + + }// paintContent + + /** + * + * This class is used as a container for the coordinates used by + * CoordinateLayout. When attached to a CoordinateLayout object, changes to + * this object are reflected on the layout. + * + */ + public static class Coordinates { + + // Length of the property arrays + protected static final int NUMBEROFPROPERTIES = 6; + + // The actual coordinates + protected final int[] properties; + + // These help to decipher the coordinates + protected final boolean[] isUnitPercent; + + // Host layout(s) + protected final ArrayList listeners = new ArrayList( + 1); + + /** + * Creates a new Coordinates object with the specified values. If the + * user does not wish to specify a value, -1 can be used to let the + * terminal decide the value. + * + * The values are treated as absolute. You can change them to be + * relative with setUnitsPercent or setAllUnitsPercent. + * + * @param top + * distance of component top from coordinateLayout top + * @param right + * distance of component right from coordinateLayout right + * @param bottom + * distance of component bottom from coordinateLayout bottom + * @param left + * distance of component left from coordinateLayout left + * @param width + * component width + * @param height + * component height + * + * @throws NumberFormatException + */ + public Coordinates(int left, int top, int width, int height, int right, + int bottom) throws IllegalArgumentException { + + properties = new int[NUMBEROFPROPERTIES]; + isUnitPercent = new boolean[NUMBEROFPROPERTIES]; + + properties[LEFT] = left; + properties[TOP] = top; + properties[WIDTH] = width; + properties[HEIGHT] = height; + properties[RIGHT] = right; + properties[BOTTOM] = bottom; + + for (int i = 0; i < NUMBEROFPROPERTIES; i++) { + if (properties[i] < -1) { + throw new IllegalArgumentException( + "Illegal coordinate value " + properties[i]); + } + } + + } + + /** + * Create a new Coordinates object with the given value string. + * + * @param coordinateString + * @throws IllegalArgumentException + * @see com.itmill.toolkit.ui.CoordinateLayout#addComponent(Component, + * String) + */ + public Coordinates(String coordinateString) + throws IllegalArgumentException { + + properties = new int[NUMBEROFPROPERTIES]; + isUnitPercent = new boolean[NUMBEROFPROPERTIES]; + + Arrays.fill(properties, -1); + + setValuesFromString(coordinateString); + + } + + /** + * Set unit of the given argument to be percentage. Initially all are + * false. + * + * @param top + * @param right + * @param bottom + * @param left + * @param width + * @param height + */ + public void setUnitsPercent(boolean left, boolean top, boolean width, + boolean height, boolean right, boolean bottom) { + isUnitPercent[TOP] = top; + isUnitPercent[LEFT] = left; + isUnitPercent[WIDTH] = width; + isUnitPercent[HEIGHT] = height; + isUnitPercent[RIGHT] = right; + isUnitPercent[BOTTOM] = bottom; + + notifyParents(); + } + + /** + * Set if a property should be treated as a percentage value. + * + * @param valueId + * either CoordinateLayout.TOP, CoordinateLayout.RIGHT, + * CoordinateLayout.BOTTOM, CoordinateLayout.LEFT, + * CoordinateLayout.WIDTH or CoordinateLayout.HEIGHT + * @param isPercent + * true if the value should be treated as a percentage, false + * otherwise + */ + public void setUnitPercent(int valueId, boolean isPercent) { + isUnitPercent[valueId] = isPercent; + + notifyParents(); + } + + /** + * Set if the values should be treated as percentage. + * + * @param value + * true if all the values should be treated as percentage, + * false if all the values should in treated as abslute pixel + * values + */ + public void setAllUnitsPercent(boolean value) { + java.util.Arrays.fill(isUnitPercent, value); + notifyParents(); + } + + /** + * Set a value for a direction. + * + * @param direction + * either CoordinateLayout.TOP, CoordinateLayout.RIGHT, + * CoordinateLayout.BOTTOM, CoordinateLayout.LEFT, + * CoordinateLayout.WIDTH or CoordinateLayout.HEIGHT + * @value the value to be set + */ + public void setCoordinate(int direction, int value) { + properties[direction] = value; + notifyParents(); + } + + /** + * Set coordinates for this object. + * + * @param top + * @param right + * @param bottom + * @param left + */ + public void setCoordinates(int left, int top, int width, int height, + int right, int bottom) { + properties[LEFT] = left; + properties[TOP] = top; + properties[WIDTH] = width; + properties[HEIGHT] = height; + properties[RIGHT] = right; + properties[BOTTOM] = bottom; + + for (int i = 0; i < NUMBEROFPROPERTIES; i++) { + if (properties[i] < -1) { + throw new IllegalArgumentException( + "Illegal coordinate value " + properties[i]); + } + } + notifyParents(); + } + + /** + * Set the values in string format. For reference, see + * {@link CoordinateLayout#addComponent(Component, String)} + * + * @param coordinateString + * @throws IllegalArgumentException + */ + public void setCoordinates(String coordinateString) + throws IllegalArgumentException { + setValuesFromString(coordinateString); + notifyParents(); + } + + /** + * Returns the coordinates for this object. + * + * @param direction + * either CoordinateLayout.TOP, CoordinateLayout.RIGHT, + * CoordinateLayout.BOTTOM, CoordinateLayout.LEFT, + * CoordinateLayout.WIDTH or CoordinateLayout.HEIGHT + * @return coordinates for the given direction + */ + public int getCoordinate(int direction) { + return properties[direction]; + } + + /** + * Check if a given value is a percentage value. The default is false. + * + * @param value + * either CoordinateLayout.TOP, CoordinateLayout.RIGHT, + * CoordinateLayout.BOTTOM, CoordinateLayout.LEFT, + * CoordinateLayout.WIDTH or CoordinateLayout.HEIGHT + * + * @return true if the given value has percentages as its unit, false + * otherwise + */ + public boolean isValuePercent(int value) { + return isUnitPercent[value]; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + public String toString() { + StringBuffer returnString = new StringBuffer(); + + for (int i = 0; i < properties.length; i++) { + returnString.append(properties[i]); + if (isUnitPercent[i]) { + returnString.append("%"); + } + if (i != properties.length - 1) { + returnString.append(","); + } + } + + return returnString.toString(); + } + + /* + * Parses the string to integer values + */ + protected void setValuesFromString(String coordinateString) { + + int[] newProperties = new int[6]; + boolean[] newPercent = new boolean[6]; + + resetContents(newProperties, newPercent); + + String coordStringArray[] = coordinateString.split(","); + if (coordStringArray.length > 6) { + throw new IllegalArgumentException( + "Incorrect string syntax: too many arguments"); + } + + Pattern numberPattern = Pattern.compile("[-]??\\d+"); + String percentRegex = ".*%.*"; + Matcher matcher = null; + try { + for (int i = 0; i < coordStringArray.length; i++) { + + matcher = numberPattern.matcher(coordStringArray[i].trim()); + if (matcher.find()) { + newProperties[i] = Integer.parseInt(matcher.group()); + + newPercent[i] = coordStringArray[i] + .matches(percentRegex); + } else { + throw new IllegalArgumentException( + "Error parsing number: " + coordStringArray[i]); + } + } + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalArgumentException("Error parsing string: " + + e.toString()); + } + + // Only set new values if there were no parsing errors + resetContents(properties, isUnitPercent); + System.arraycopy(newProperties, 0, properties, 0, + NUMBEROFPROPERTIES); + System.arraycopy(newPercent, 0, isUnitPercent, 0, + NUMBEROFPROPERTIES); + } + + /* + * Reset coordinate properties. Does not notify listeners + */ + protected void resetContents(int[] prop, boolean[] perc) { + Arrays.fill(prop, -1); + Arrays.fill(perc, false); + } + + /* + * These methods ensure that changes to the coordinate objects are + * reflected to the host layout + */ + protected void addParentLayout(CoordinateLayout listener) { + listeners.add(listener); + } + + protected void removeParentLayout(CoordinateLayout listener) { + listeners.remove(listener); + } + + protected void notifyParents() { + for (int i = 0; i < listeners.size(); i++) { + ((CoordinateLayout) listeners.get(i)).requestRepaint(); + } + } + }// class Coordinates + +}// class CoordinateLayout +