package com.vaadin.incubator.dashlayout.client.ui; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import com.google.gwt.dom.client.Document; import com.google.gwt.event.dom.client.DomEvent.Type; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.ComplexPanel; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.user.client.ui.WidgetCollection; import com.vaadin.incubator.dashlayout.client.util.css.CSSRule; import com.vaadin.incubator.dashlayout.client.util.css.CSSUtil; 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.Paintable; 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.ValueMap; import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; import com.vaadin.terminal.gwt.client.ui.VMarginInfo; public class VDashLayout extends ComplexPanel implements Container { public static final String CLASSNAME = "v-dashlayout"; public static final String CLICK_EVENT_IDENTIFIER = "click"; private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler( this, CLICK_EVENT_IDENTIFIER) { @Override protected Paintable getChildComponent(Element element) { return getComponent(element); } @Override protected <H extends EventHandler> HandlerRegistration registerHandler( H handler, Type<H> type) { return addDomHandler(handler, type); } }; private Paintable getComponent(Element element) { return Util.getChildPaintableForElement(client, VDashLayout.this, element); } protected boolean horizontal = false; /** * A logical mapping from widget to its virtual layout cell. */ protected Map<Widget, ChildCell> widgetToCell = new HashMap<Widget, ChildCell>(); /** * All array values are in the following order: top, right, bottom, left. */ /* Current margin values */ protected int[] margin = { 0, 0, 0, 0 }; /* Current padding values */ protected int[] padding = { 0, 0, 0, 0 }; /* Current border sizes */ protected int[] border = { 0, 0, 0, 0 }; /* * Current inner size (excluding margins, border and padding) in pixels */ protected int width = -1; protected int height = -1; protected boolean isRendering; protected boolean sizeHasChangedDuringRendering = false; // Is the layout size undefined, i.e. defined by the contained components protected boolean undefWidth = false; protected boolean undefHeight = false; protected ApplicationConnection client; protected String lastStyleName; protected VMarginInfo marginInfo; protected float compoundRatio = -1; protected int consumedSpace = 0; protected boolean useSpacing = true; protected HashMap<String, Integer> layoutDetails = new HashMap<String, Integer>(); public VDashLayout() { super(); setElement(Document.get().createDivElement()); setStylePrimaryName(CLASSNAME); getElement().getStyle().setProperty("float", "left"); getElement().getStyle().setProperty("cssFloat", "left"); getElement().getStyle().setProperty("styleFloat", "left"); getElement().getStyle().setProperty("display", "inline"); if (BrowserInfo.get().isIE()) { getElement().getStyle().setProperty("zoom", "1"); } } public boolean isHorizontal() { return horizontal; } @Override public WidgetCollection getChildren() { return super.getChildren(); } public int getSpacing() { if (layoutDetails.containsKey("spacing")) { return layoutDetails.get("spacing"); } else { return 0; } } public float getCompoundRatio() { return compoundRatio; } public Map<Widget, ChildCell> getCells() { return widgetToCell; } public int getConsumedSpace() { return consumedSpace; } public void updateActualSize() { width = CSSUtil .parsePixel(CSSUtil.getStyleValue(getElement(), "width")); height = CSSUtil.parsePixel(CSSUtil.getStyleValue(getElement(), "height")); } private void updateMargins() { margin = CSSUtil.collectMargin(getElement()); if (marginInfo.hasTop()) { getElement().getStyle().setProperty("marginTop", ""); } else { getElement().getStyle().setProperty("marginTop", "0"); margin[0] = 0; } if (marginInfo.hasRight()) { getElement().getStyle().setProperty("marginRight", ""); } else { getElement().getStyle().setProperty("marginRight", "0"); margin[1] = 0; } if (marginInfo.hasBottom()) { getElement().getStyle().setProperty("marginBottom", ""); } else { getElement().getStyle().setProperty("marginBottom", "0"); margin[2] = 0; } if (marginInfo.hasLeft()) { getElement().getStyle().setProperty("marginLeft", ""); } else { getElement().getStyle().setProperty("marginLeft", "0"); margin[3] = 0; } } private void updateDynamicSizeInfo(UIDL uidl) { String w = uidl.hasAttribute("width") ? uidl .getStringAttribute("width") : ""; undefWidth = w.equals(""); String h = uidl.hasAttribute("height") ? uidl .getStringAttribute("height") : ""; undefHeight = h.equals(""); } private void measureLayoutDetails() { margin = CSSUtil.collectMargin(getElement()); padding = CSSUtil.collectPadding(getElement()); border = CSSUtil.collectBorder(getElement()); final String[] styles = getElement().getClassName().split(" "); for (int i = styles.length - 1; i >= 0; i--) { String style = styles[i]; final CSSRule r = new CSSRule("#" + style + "-details", false); if (useSpacing) { // We misuse "letter-spacing" property for our layout // spacing value final String spacing = r.getPropertyValue("letterSpacing"); if (spacing != null && !layoutDetails.containsKey("spacing")) { layoutDetails.put("spacing", CSSUtil.parsePixel(spacing)); } } final String minWidth = r.getPropertyValue("minWidth"); if (minWidth != null && !layoutDetails.containsKey("minWidth")) { layoutDetails.put("minWidth", CSSUtil.parsePixel(minWidth)); } final String maxWidth = r.getPropertyValue("maxWidth"); if (maxWidth != null && !layoutDetails.containsKey("maxWidth")) { layoutDetails.put("maxWidth", CSSUtil.parsePixel(maxWidth)); } final String minHeight = r.getPropertyValue("minHeight"); if (minHeight != null && !layoutDetails.containsKey("minHeight")) { layoutDetails.put("minHeight", CSSUtil.parsePixel(minHeight)); } final String maxHeight = r.getPropertyValue("maxHeight"); if (maxHeight != null && !layoutDetails.containsKey("maxHeight")) { layoutDetails.put("maxHeight", CSSUtil.parsePixel(maxHeight)); } } } @Override public void setStyleName(String styleName) { super.setStyleName(styleName); if (isAttached() && !styleName.equals(lastStyleName)) { measureLayoutDetails(); // updateChildCells(null, false); lastStyleName = styleName; } } public RenderSpace getAllocatedSpace(Widget child) { ChildCell cell = widgetToCell.get(child); if (child instanceof VDashLayout) { // Layout handles it's margins internally return cell.getRenderSpace(); } return cell.getSpaceSansMargins(); } public boolean hasChildComponent(Widget component) { return getChildren().contains(component); } public void replaceChildComponent(Widget oldComponent, Widget newComponent) { if (!hasChildComponent(oldComponent)) { return; } getChildren().insert(newComponent, getChildren().indexOf(oldComponent)); getChildren().remove(oldComponent); getElement().insertBefore(newComponent.getElement(), oldComponent.getElement()); getElement().removeChild(oldComponent.getElement()); orphan(oldComponent); adopt(newComponent); widgetToCell.remove(oldComponent); ChildCell cell = new ChildCell(newComponent, this); widgetToCell.put(newComponent, cell); } public void updateCaption(Paintable component, UIDL uidl) { // TODO Use this same class to display component captions // TODO I need to make this class usable without the server-side } /** * Only pixel values are accepted. */ @Override public void setWidth(String w) { // Assume pixel values are always passed from ApplicationConnection int oldW = width; String toBeWidth = ""; if (w != null && w != "") { int newWidth = CSSUtil.parsePixel(w) - margin[1] - margin[3] - border[1] - border[3] - padding[1] - padding[3]; if (newWidth < 0) { newWidth = 0; } width = newWidth; toBeWidth = newWidth + "px"; } else { width = -1; } final Integer minWidth = layoutDetails.get("minWidth"); if (minWidth != null && width < minWidth) { width = minWidth.intValue(); toBeWidth = width + "px"; } final Integer maxWidth = layoutDetails.get("maxWidth"); if (maxWidth != null && width > maxWidth) { width = maxWidth.intValue(); toBeWidth = width + "px"; } super.setWidth(toBeWidth); updateActualSize(); if (width != oldW) { if (isRendering) { sizeHasChangedDuringRendering = true; } updateLayout(false); client.runDescendentsLayout(this); } } /** * Only pixel values are accepted. */ @Override public void setHeight(String h) { // Assume pixel values are always passed from ApplicationConnection int oldH = height; String toBeHeight = ""; if (h != null && h != "") { int newHeight = CSSUtil.parsePixel(h) - margin[0] - margin[2] - border[0] - border[2] - padding[0] - padding[2]; if (newHeight < 0) { newHeight = 0; } height = newHeight; toBeHeight = newHeight + "px"; } else { height = -1; } final Integer minHeight = layoutDetails.get("minHeight"); if (minHeight != null && height < minHeight) { height = minHeight.intValue(); toBeHeight = height + "px"; } final Integer maxHeight = layoutDetails.get("maxHeight"); if (maxHeight != null && height > maxHeight) { height = maxHeight.intValue(); toBeHeight = height + "px"; } super.setHeight(toBeHeight); updateActualSize(); if (height != oldH) { if (isRendering) { sizeHasChangedDuringRendering = true; } updateLayout(false); client.runDescendentsLayout(this); } } public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { this.client = client; // Only non-cached UIDL messages can introduce changes if (uidl.getBooleanAttribute("cached")) { return; } useSpacing = uidl.hasAttribute("spacing"); // Margins need to be set before any size calculations int bitMask = uidl.getIntAttribute("margins"); if (marginInfo == null || marginInfo.getBitMask() != bitMask) { marginInfo = new VMarginInfo(bitMask); updateMargins(); } else if (marginInfo.getBitMask() == bitMask) { // interpret as requestRepaint requested. Could be just an update on // margins or expand ratios, but we need to update the margin, // border and padding values in this case measureLayoutDetails(); // Otherwise margins and spacing are measured in // #setStyleName(String), inside client.updateComponent } if (client.updateComponent(this, uidl, true)) { return; } clickEventHandler.handleEventHandlerRegistration(client); horizontal = uidl.hasAttribute("horizontal"); isRendering = true; updateDynamicSizeInfo(uidl); // Iterate through Paintables in UIDL, add new ones and remove any // old ones. final int uidlCount = uidl.getChildCount(); final int layoutCount = getChildren().size(); int uidlPos = 0; int layoutPos = 0; // Additional info that needs to be passed to components final ValueMap alignments = uidl.getMapAttribute("alignments"); final ValueMap expandRatios = uidl.getMapAttribute("expandRatios"); if (expandRatios.getKeySet().size() > 0) { compoundRatio = 0; } else { compoundRatio = -1; } for (; (uidlPos < uidlCount || layoutPos < layoutCount); uidlPos++) { final UIDL childUIDL = (uidlPos < uidlCount) ? uidl .getChildUIDL(uidlPos) : null; final Widget uidlWidget = childUIDL != null ? (Widget) client .getPaintable(childUIDL) : null; final Widget layoutWidget = (layoutPos < layoutCount) ? getChildren() .get(layoutPos) : null; // layout position is past UIDL position, no need to continue // (remaining old widgets are removed after this loop) if (uidlPos >= uidlCount && layoutPos < layoutCount) { break; } // Old widget in old location, all OK if (uidlWidget == layoutWidget) { layoutPos++; } // Widget is either new or has changed place else if (getChildren().contains(uidlWidget)) { // An old component has changed place if (getChildren().indexOf(uidlWidget) != layoutPos) { // Detach from old position child. uidlWidget.removeFromParent(); // Logical attach. getChildren().insert(uidlWidget, layoutPos); getElement().insertBefore(uidlWidget.getElement(), getElement().getChildNodes().getItem(layoutPos)); adopt(uidlWidget); layoutPos++; } } else { // A completely new widget is inserted to this position ChildCell cell = new ChildCell(uidlWidget, this); widgetToCell.put(uidlWidget, cell); // Logical attach getChildren().insert(uidlWidget, layoutPos); // Avoid inserts (they are slower than appends) if (layoutPos < getChildren().size() - 1) { getElement().insertBefore(uidlWidget.getElement(), getElement().getChildNodes().getItem(layoutPos)); } else { getElement().appendChild(uidlWidget.getElement()); } layoutPos++; // Adopt adopt(uidlWidget); } ChildCell cell = widgetToCell.get(uidlWidget); cell.updateSizeInfo(childUIDL); cell.updateSpace(); ((Paintable) uidlWidget).updateFromUIDL(childUIDL, client); if (alignments.containsKey(childUIDL.getId())) { cell.setAlignment(alignments.getInt(childUIDL.getId())); } if (expandRatios.containsKey(childUIDL.getId())) { final float ratio = expandRatios.getInt(childUIDL.getId()); compoundRatio += ratio; cell.setExpandRatio(ratio); } } // All UIDL widgets painted // All remaining widgets are removed removeChildrenAfter(layoutPos); updateLayout(false); isRendering = false; if (sizeHasChangedDuringRendering) { updateLayout(true); } } protected void removeChildrenAfter(int pos) { int toRemove = getChildren().size() - pos; while (toRemove-- > 0) { Widget child = getChildren().get(pos); widgetToCell.remove(child); remove(child); client.unregisterPaintable((Paintable) child); } } protected void updateLayout(boolean reset) { // Hide overflows for the time of layouting getElement().getStyle().setProperty("overflow", "hidden"); if (undefWidth) { super.setWidth(""); } if (undefHeight) { super.setHeight(""); } if (undefWidth || undefHeight) { updateActualSize(); } consumedSpace = 0; int totalSize = 0; int biggestSize = 0; if (useSpacing) { totalSize += getSpacing() * (getChildren().size() - 1); } final ArrayList<ChildCell> updateAfter = new ArrayList<ChildCell>(); for (Widget w : getChildren()) { final ChildCell cell = getCells().get(w); if (reset) { // Reset widget size as well (true) cell.reset(true); } if (cell.updateAfterOtherCells()) { updateAfter.add(cell); if (!cell.isRelativeSizeInParentOrientation()) { cell.updateWidgetMarginAndSize(); totalSize += isHorizontal() ? cell.getWidgetSize() .getWidth() : cell.getWidgetSize().getHeight(); } } else { cell.updateWidgetMarginAndSize(); cell.updateSpace(); cell.reAlign(); totalSize += cell.getMaxSizeInParentOrientation(); final int size = cell.getMaxSizeInNonParentOrientation(); if (size > biggestSize) { biggestSize = size; } } } if (undefHeight && isHorizontal()) { height = biggestSize; } else if (undefWidth) { width = biggestSize; } // Update consumed space before calculating expand ratio sizes consumedSpace = totalSize; for (int i = 0; i < updateAfter.size(); i++) { ChildCell cell = updateAfter.get(i); // Reclaim previously reserved space (added back later) if (!cell.isRelativeSizeInParentOrientation()) { totalSize -= isHorizontal() ? cell.getWidgetSize().getWidth() : cell.getWidgetSize().getHeight(); } cell.updateWidgetMargin(); cell.updateSpace(); if (cell.hasRelativeSize()) { client.handleComponentRelativeSize(cell.getWidget()); cell.updateWidgetSize(); } cell.reAlign(); final int size = cell.getMaxSizeInNonParentOrientation(); if (size > biggestSize) { biggestSize = size; } totalSize += cell.getMaxSizeInParentOrientation(); } if (updateAfter.size() > 1 && ((isHorizontal() && !undefWidth) || !undefHeight)) { ChildCell last = updateAfter.get(updateAfter.size() - 1); // Add rounding error pixel to last child (if more than one found, // otherwise no rounding errors should exhibit) totalSize -= last.getMaxSizeInParentOrientation(); if (isHorizontal()) { last.getRenderSpace().setWidth(width - totalSize); } else { last.getRenderSpace().setHeight(height - totalSize); } if (last.hasRelativeSize()) { client.handleComponentRelativeSize(last.getWidget()); last.updateWidgetSize(); } last.reAlign(); totalSize += last.getMaxSizeInParentOrientation(); } if (undefWidth) { width = isHorizontal() ? totalSize : biggestSize; super.setWidth(width + "px"); } if (undefHeight) { height = isHorizontal() ? biggestSize : totalSize; super.setHeight(height + "px"); } getElement().getStyle().setProperty("overflow", ""); } public boolean requestLayout(Set<Paintable> children) { int oldWidth = width; int oldHeight = height; updateLayout(false); if (undefWidth && oldWidth != width) { return false; } if (undefHeight && oldHeight != height) { return false; } return true; // TODO this could probably be optimized with a code like below // for (Paintable child : children) { // ChildCell cell = widgetToCell.get(child); // cell.reset(false); // cell.updateSpace(); // cell.updateNaturalDimensions(); // cell.reAlign(); // } // if (undefWidth) { // final int oldWidth = width; // updateActualSize(); // if (width != oldWidth) { // for (Widget child : getChildren()) { // ChildCell cell = widgetToCell.get(child); // cell.reset(false); // cell.updateSpace(); // if (cell.isRelativeWidth() || cell.isRelativeHeight()) { // client.handleComponentRelativeSize(child); // } // cell.updateNaturalDimensions(); // cell.reAlign(); // } // return false; // } // } // return true; } }