com.vaadin.client.ui.layout.LayoutDependencyTree Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-client Show documentation
Show all versions of vaadin-client Show documentation
Vaadin is a web application framework for Rich Internet Applications (RIA).
Vaadin enables easy development and maintenance of fast and
secure rich web
applications with a stunning look and feel and a wide browser support.
It features a server-side architecture with the majority of the logic
running
on the server. Ajax technology is used at the browser-side to ensure a
rich
and interactive user experience.
/*
* Copyright 2000-2014 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.client.ui.layout;
import java.util.ArrayList;
import java.util.Collection;
import com.google.gwt.core.client.JsArrayString;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.FastStringMap;
import com.vaadin.client.FastStringSet;
import com.vaadin.client.HasComponentsConnector;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.Profiler;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.VConsole;
import com.vaadin.client.ui.ManagedLayout;
import com.vaadin.shared.AbstractComponentState;
/**
* Internal class used to keep track of layout dependencies during one layout
* run. This class is not intended to be used directly by applications.
*
* @author Vaadin Ltd
* @since 7.0.0
*/
public class LayoutDependencyTree {
private class LayoutDependency {
private final ComponentConnector connector;
private final int direction;
private boolean needsLayout = false;
private boolean needsMeasure = false;
private boolean scrollingParentCached = false;
private ComponentConnector scrollingBoundary = null;
private FastStringSet measureBlockers = FastStringSet.create();
private FastStringSet layoutBlockers = FastStringSet.create();
public LayoutDependency(ComponentConnector connector, int direction) {
this.connector = connector;
this.direction = direction;
}
private void addLayoutBlocker(ComponentConnector blocker) {
String blockerId = blocker.getConnectorId();
if (!layoutBlockers.contains(blockerId)) {
boolean wasEmpty = layoutBlockers.isEmpty();
layoutBlockers.add(blockerId);
if (wasEmpty) {
if (needsLayout) {
getLayoutQueue(direction).remove(
connector.getConnectorId());
} else {
// Propagation already done if needsLayout is set
propagatePotentialLayout();
}
}
}
}
private void removeLayoutBlocker(ComponentConnector blocker) {
String blockerId = blocker.getConnectorId();
if (layoutBlockers.contains(blockerId)) {
layoutBlockers.remove(blockerId);
if (layoutBlockers.isEmpty()) {
if (needsLayout) {
getLayoutQueue(direction).add(
connector.getConnectorId());
} else {
propagateNoUpcomingLayout();
}
}
}
}
private void addMeasureBlocker(ComponentConnector blocker) {
String blockerId = blocker.getConnectorId();
boolean alreadyAdded = measureBlockers.contains(blockerId);
if (alreadyAdded) {
return;
}
boolean wasEmpty = measureBlockers.isEmpty();
measureBlockers.add(blockerId);
if (wasEmpty) {
if (needsMeasure) {
getMeasureQueue(direction).remove(
connector.getConnectorId());
} else {
propagatePotentialResize();
}
}
}
private void removeMeasureBlocker(ComponentConnector blocker) {
String blockerId = blocker.getConnectorId();
boolean alreadyRemoved = !measureBlockers.contains(blockerId);
if (alreadyRemoved) {
return;
}
measureBlockers.remove(blockerId);
if (measureBlockers.isEmpty()) {
if (needsMeasure) {
getMeasureQueue(direction).add(connector.getConnectorId());
} else {
propagateNoUpcomingResize();
}
}
}
public void setNeedsMeasure(boolean needsMeasure) {
if (needsMeasure && !this.needsMeasure) {
// If enabling needsMeasure
this.needsMeasure = needsMeasure;
if (measureBlockers.isEmpty()) {
// Add to queue if there are no blockers
getMeasureQueue(direction).add(connector.getConnectorId());
// Only need to propagate if not already propagated when
// setting blockers
propagatePotentialResize();
}
} else if (!needsMeasure && this.needsMeasure
&& measureBlockers.isEmpty()) {
// Only disable if there are no blockers (elements gets measured
// in both directions even if there is a blocker in one
// direction)
this.needsMeasure = needsMeasure;
getMeasureQueue(direction).remove(connector.getConnectorId());
propagateNoUpcomingResize();
}
}
public void setNeedsLayout(boolean needsLayout) {
if (!(connector instanceof ManagedLayout)) {
throw new IllegalStateException(
"Only managed layouts can need layout, layout attempted for "
+ Util.getConnectorString(connector));
}
if (needsLayout && !this.needsLayout) {
// If enabling needsLayout
this.needsLayout = needsLayout;
if (layoutBlockers.isEmpty()) {
// Add to queue if there are no blockers
getLayoutQueue(direction).add(connector.getConnectorId());
// Only need to propagate if not already propagated when
// setting blockers
propagatePotentialLayout();
}
} else if (!needsLayout && this.needsLayout
&& layoutBlockers.isEmpty()) {
// Only disable if there are no layout blockers
// (SimpleManagedLayout gets layouted in both directions
// even if there is a blocker in one direction)
this.needsLayout = needsLayout;
getLayoutQueue(direction).remove(connector.getConnectorId());
propagateNoUpcomingLayout();
}
}
private void propagatePotentialResize() {
JsArrayString needsSizeForLayout = getNeedsSizeForLayout();
int length = needsSizeForLayout.length();
for (int i = 0; i < length; i++) {
String needsSizeId = needsSizeForLayout.get(i);
LayoutDependency layoutDependency = getDependency(needsSizeId,
direction);
layoutDependency.addLayoutBlocker(connector);
}
}
private JsArrayString getNeedsSizeForLayout() {
// Find all connectors that need the size of this connector for
// layouting
// Parent needs size if it isn't relative?
// Connector itself needs size if it isn't undefined?
// Children doesn't care?
JsArrayString needsSize = JsArrayObject.createArray().cast();
if (!isUndefinedInDirection(connector, direction)) {
needsSize.push(connector.getConnectorId());
}
if (!isRelativeInDirection(connector, direction)) {
ServerConnector parent = connector.getParent();
if (parent instanceof ComponentConnector) {
needsSize.push(parent.getConnectorId());
}
}
return needsSize;
}
private void propagateNoUpcomingResize() {
JsArrayString needsSizeForLayout = getNeedsSizeForLayout();
int length = needsSizeForLayout.length();
for (int i = 0; i < length; i++) {
String mightNeedLayoutId = needsSizeForLayout.get(i);
LayoutDependency layoutDependency = getDependency(
mightNeedLayoutId, direction);
layoutDependency.removeLayoutBlocker(connector);
}
}
private void propagatePotentialLayout() {
JsArrayString resizedByLayout = getResizedByLayout();
int length = resizedByLayout.length();
for (int i = 0; i < length; i++) {
String sizeMightChangeId = resizedByLayout.get(i);
LayoutDependency layoutDependency = getDependency(
sizeMightChangeId, direction);
layoutDependency.addMeasureBlocker(connector);
}
}
private JsArrayString getResizedByLayout() {
// Components that might get resized by a layout of this component
// Parent never resized
// Connector itself resized if undefined
// Children resized if relative
JsArrayString resized = JsArrayObject.createArray().cast();
if (isUndefinedInDirection(connector, direction)) {
resized.push(connector.getConnectorId());
}
if (connector instanceof HasComponentsConnector) {
HasComponentsConnector container = (HasComponentsConnector) connector;
for (ComponentConnector child : container.getChildComponents()) {
if (isRelativeInDirection(child, direction)) {
resized.push(child.getConnectorId());
}
}
}
return resized;
}
private void propagateNoUpcomingLayout() {
JsArrayString resizedByLayout = getResizedByLayout();
int length = resizedByLayout.length();
for (int i = 0; i < length; i++) {
String sizeMightChangeId = resizedByLayout.get(i);
LayoutDependency layoutDependency = getDependency(
sizeMightChangeId, direction);
layoutDependency.removeMeasureBlocker(connector);
}
}
public void markSizeAsChanged() {
Profiler.enter("LayoutDependency.markSizeAsChanged phase 1");
// When the size has changed, all that use that size should be
// layouted
JsArrayString needsSizeForLayout = getNeedsSizeForLayout();
int length = needsSizeForLayout.length();
for (int i = 0; i < length; i++) {
String connectorId = needsSizeForLayout.get(i);
LayoutDependency layoutDependency = getDependency(connectorId,
direction);
if (layoutDependency.connector instanceof ManagedLayout) {
Profiler.enter("LayoutDependency.markSizeAsChanged setNeedsLayout");
layoutDependency.setNeedsLayout(true);
Profiler.leave("LayoutDependency.markSizeAsChanged setNeedsLayout");
} else {
Profiler.enter("LayoutDependency.markSizeAsChanged propagatePostLayoutMeasure");
// Should simulate setNeedsLayout(true) + markAsLayouted ->
// propagate needs measure
layoutDependency.propagatePostLayoutMeasure();
Profiler.leave("LayoutDependency.markSizeAsChanged propagatePostLayoutMeasure");
}
}
Profiler.leave("LayoutDependency.markSizeAsChanged phase 1");
Profiler.enter("LayoutDependency.markSizeAsChanged scrollbars");
// Should also go through the hierarchy to discover appeared or
// disappeared scrollbars
ComponentConnector scrollingBoundary = getScrollingBoundary(connector);
if (scrollingBoundary != null) {
getDependency(scrollingBoundary.getConnectorId(),
getOppositeDirection()).setNeedsMeasure(true);
}
Profiler.leave("LayoutDependency.markSizeAsChanged scrollbars");
}
/**
* Go up the hierarchy to find a component whose size might have changed
* in the other direction because changes to this component causes
* scrollbars to appear or disappear.
*
* @return
*/
private LayoutDependency findPotentiallyChangedScrollbar() {
ComponentConnector currentConnector = connector;
while (true) {
ServerConnector parent = currentConnector.getParent();
if (!(parent instanceof ComponentConnector)) {
return null;
}
if (parent instanceof MayScrollChildren) {
return getDependency(currentConnector.getConnectorId(),
getOppositeDirection());
}
currentConnector = (ComponentConnector) parent;
}
}
private int getOppositeDirection() {
return direction == HORIZONTAL ? VERTICAL : HORIZONTAL;
}
public void markAsLayouted() {
if (!layoutBlockers.isEmpty()) {
// Don't do anything if there are layout blockers (SimpleLayout
// gets layouted in both directions even if one direction is
// blocked)
return;
}
setNeedsLayout(false);
propagatePostLayoutMeasure();
}
private void propagatePostLayoutMeasure() {
Profiler.enter("LayoutDependency.propagatePostLayoutMeasure getResizedByLayout");
JsArrayString resizedByLayout = getResizedByLayout();
Profiler.leave("LayoutDependency.propagatePostLayoutMeasure getResizedByLayout");
int length = resizedByLayout.length();
for (int i = 0; i < length; i++) {
Profiler.enter("LayoutDependency.propagatePostLayoutMeasure setNeedsMeasure");
String resizedId = resizedByLayout.get(i);
LayoutDependency layoutDependency = getDependency(resizedId,
direction);
layoutDependency.setNeedsMeasure(true);
Profiler.leave("LayoutDependency.propagatePostLayoutMeasure setNeedsMeasure");
}
// Special case for e.g. wrapping texts
Profiler.enter("LayoutDependency.propagatePostLayoutMeasure horizontal case");
if (direction == HORIZONTAL && !connector.isUndefinedWidth()
&& connector.isUndefinedHeight()) {
LayoutDependency dependency = getDependency(
connector.getConnectorId(), VERTICAL);
dependency.setNeedsMeasure(true);
}
Profiler.leave("LayoutDependency.propagatePostLayoutMeasure horizontal case");
}
@Override
public String toString() {
String s = getCompactConnectorString(connector) + "\n";
if (direction == VERTICAL) {
s += "Vertical";
} else {
s += "Horizontal";
}
AbstractComponentState state = connector.getState();
s += " sizing: "
+ getSizeDefinition(direction == VERTICAL ? state.height
: state.width) + "\n";
if (needsLayout) {
s += "Needs layout\n";
}
if (getLayoutQueue(direction).contains(connector.getConnectorId())) {
s += "In layout queue\n";
}
s += "Layout blockers: " + blockersToString(layoutBlockers) + "\n";
if (needsMeasure) {
s += "Needs measure\n";
}
if (getMeasureQueue(direction).contains(connector.getConnectorId())) {
s += "In measure queue\n";
}
s += "Measure blockers: " + blockersToString(measureBlockers);
return s;
}
public boolean noMoreChangesExpected() {
return !needsLayout && !needsMeasure && layoutBlockers.isEmpty()
&& measureBlockers.isEmpty();
}
}
private static final int HORIZONTAL = 0;
private static final int VERTICAL = 1;
@SuppressWarnings("unchecked")
private final FastStringMap[] dependenciesInDirection = new FastStringMap[] {
FastStringMap.create(), FastStringMap.create() };
private final FastStringSet[] measureQueueInDirection = new FastStringSet[] {
FastStringSet.create(), FastStringSet.create() };
private final FastStringSet[] layoutQueueInDirection = new FastStringSet[] {
FastStringSet.create(), FastStringSet.create() };
private final ApplicationConnection connection;
public LayoutDependencyTree(ApplicationConnection connection) {
this.connection = connection;
}
public void setNeedsMeasure(ComponentConnector connector,
boolean needsMeasure) {
setNeedsHorizontalMeasure(connector, needsMeasure);
setNeedsVerticalMeasure(connector, needsMeasure);
}
public void setNeedsMeasure(String connectorId, boolean needsMeasure) {
ComponentConnector connector = (ComponentConnector) ConnectorMap.get(
connection).getConnector(connectorId);
if (connector == null) {
return;
}
setNeedsMeasure(connector, needsMeasure);
}
public void setNeedsHorizontalMeasure(ComponentConnector connector,
boolean needsMeasure) {
LayoutDependency dependency = getDependency(connector, HORIZONTAL);
dependency.setNeedsMeasure(needsMeasure);
}
public void setNeedsHorizontalMeasure(String connectorId,
boolean needsMeasure) {
// Ensure connector exists
ComponentConnector connector = (ComponentConnector) ConnectorMap.get(
connection).getConnector(connectorId);
if (connector == null) {
return;
}
setNeedsHorizontalMeasure(connector, needsMeasure);
}
public void setNeedsVerticalMeasure(ComponentConnector connector,
boolean needsMeasure) {
LayoutDependency dependency = getDependency(connector, VERTICAL);
dependency.setNeedsMeasure(needsMeasure);
}
public void setNeedsVerticalMeasure(String connectorId, boolean needsMeasure) {
// Ensure connector exists
ComponentConnector connector = (ComponentConnector) ConnectorMap.get(
connection).getConnector(connectorId);
if (connector == null) {
return;
}
setNeedsVerticalMeasure(connector, needsMeasure);
}
private LayoutDependency getDependency(ComponentConnector connector,
int direction) {
return getDependency(connector.getConnectorId(), connector, direction);
}
private LayoutDependency getDependency(String connectorId, int direction) {
return getDependency(connectorId, null, direction);
}
private LayoutDependency getDependency(String connectorId,
ComponentConnector connector, int direction) {
FastStringMap dependencies = dependenciesInDirection[direction];
LayoutDependency dependency = dependencies.get(connectorId);
if (dependency == null) {
if (connector == null) {
connector = (ComponentConnector) ConnectorMap.get(connection)
.getConnector(connectorId);
}
dependency = new LayoutDependency(connector, direction);
dependencies.put(connectorId, dependency);
}
return dependency;
}
private FastStringSet getLayoutQueue(int direction) {
return layoutQueueInDirection[direction];
}
private FastStringSet getMeasureQueue(int direction) {
return measureQueueInDirection[direction];
}
/**
* @param layout
* @param needsLayout
*
* @deprecated As of 7.0.1, use
* {@link #setNeedsHorizontalLayout(String, boolean)} for
* improved performance.
*/
@Deprecated
public void setNeedsHorizontalLayout(ManagedLayout layout,
boolean needsLayout) {
setNeedsHorizontalLayout(layout.getConnectorId(), needsLayout);
}
public void setNeedsHorizontalLayout(String connectorId, boolean needsLayout) {
LayoutDependency dependency = getDependency(connectorId, HORIZONTAL);
dependency.setNeedsLayout(needsLayout);
}
/**
* @param layout
* @param needsLayout
*
* @deprecated As of 7.0.1, use
* {@link #setNeedsVerticalLayout(String, boolean)} for improved
* performance.
*/
@Deprecated
public void setNeedsVerticalLayout(ManagedLayout layout, boolean needsLayout) {
setNeedsVerticalLayout(layout.getConnectorId(), needsLayout);
}
public void setNeedsVerticalLayout(String connectorId, boolean needsLayout) {
LayoutDependency dependency = getDependency(connectorId, VERTICAL);
dependency.setNeedsLayout(needsLayout);
}
public void markAsHorizontallyLayouted(ManagedLayout layout) {
LayoutDependency dependency = getDependency(layout.getConnectorId(),
HORIZONTAL);
dependency.markAsLayouted();
}
public void markAsVerticallyLayouted(ManagedLayout layout) {
LayoutDependency dependency = getDependency(layout.getConnectorId(),
VERTICAL);
dependency.markAsLayouted();
}
public void markHeightAsChanged(ComponentConnector connector) {
LayoutDependency dependency = getDependency(connector.getConnectorId(),
VERTICAL);
dependency.markSizeAsChanged();
}
public void markWidthAsChanged(ComponentConnector connector) {
LayoutDependency dependency = getDependency(connector.getConnectorId(),
HORIZONTAL);
dependency.markSizeAsChanged();
}
private static boolean isRelativeInDirection(ComponentConnector connector,
int direction) {
if (direction == HORIZONTAL) {
return connector.isRelativeWidth();
} else {
return connector.isRelativeHeight();
}
}
private static boolean isUndefinedInDirection(ComponentConnector connector,
int direction) {
if (direction == VERTICAL) {
return connector.isUndefinedHeight();
} else {
return connector.isUndefinedWidth();
}
}
private static String getCompactConnectorString(ServerConnector connector) {
return connector.getClass().getSimpleName() + " ("
+ connector.getConnectorId() + ")";
}
private static String getSizeDefinition(String size) {
if (size == null || size.length() == 0) {
return "undefined";
} else if (size.endsWith("%")) {
return "relative";
} else {
return "fixed";
}
}
private String blockersToString(FastStringSet blockers) {
StringBuilder b = new StringBuilder("[");
ConnectorMap connectorMap = ConnectorMap.get(connection);
JsArrayString blockersDump = blockers.dump();
for (int i = 0; i < blockersDump.length(); i++) {
ServerConnector blocker = connectorMap.getConnector(blockersDump
.get(i));
if (b.length() != 1) {
b.append(", ");
}
b.append(getCompactConnectorString(blocker));
}
b.append(']');
return b.toString();
}
public boolean hasConnectorsToMeasure() {
return !measureQueueInDirection[HORIZONTAL].isEmpty()
|| !measureQueueInDirection[VERTICAL].isEmpty();
}
public boolean hasHorizontalConnectorToLayout() {
return !getLayoutQueue(HORIZONTAL).isEmpty();
}
public boolean hasVerticaConnectorToLayout() {
return !getLayoutQueue(VERTICAL).isEmpty();
}
/**
* @return
* @deprecated As of 7.0.1, use {@link #getHorizontalLayoutTargetsJsArray()}
* for improved performance.
*/
@Deprecated
public ManagedLayout[] getHorizontalLayoutTargets() {
return asManagedLayoutArray(getHorizontalLayoutTargetsJsArray());
}
/**
* @return
* @deprecated As of 7.0.1, use {@link #getVerticalLayoutTargetsJsArray()}
* for improved performance.
*/
@Deprecated
public ManagedLayout[] getVerticalLayoutTargets() {
return asManagedLayoutArray(getVerticalLayoutTargetsJsArray());
}
private ManagedLayout[] asManagedLayoutArray(JsArrayString connectorIdArray) {
int length = connectorIdArray.length();
ConnectorMap connectorMap = ConnectorMap.get(connection);
ManagedLayout[] result = new ManagedLayout[length];
for (int i = 0; i < length; i++) {
result[i] = (ManagedLayout) connectorMap
.getConnector(connectorIdArray.get(i));
}
return result;
}
public JsArrayString getHorizontalLayoutTargetsJsArray() {
return getLayoutQueue(HORIZONTAL).dump();
}
public JsArrayString getVerticalLayoutTargetsJsArray() {
return getLayoutQueue(VERTICAL).dump();
}
/**
* @return
* @deprecated As of 7.0.1, use {@link #getMeasureTargetsJsArray()} for
* improved performance.
*/
@Deprecated
public Collection getMeasureTargets() {
JsArrayString targetIds = getMeasureTargetsJsArray();
int length = targetIds.length();
ArrayList targets = new ArrayList(
length);
ConnectorMap connectorMap = ConnectorMap.get(connection);
for (int i = 0; i < length; i++) {
targets.add((ComponentConnector) connectorMap
.getConnector(targetIds.get(i)));
}
return targets;
}
public JsArrayString getMeasureTargetsJsArray() {
FastStringSet horizontalQueue = getMeasureQueue(HORIZONTAL);
JsArrayString measureTargets = horizontalQueue.dump();
JsArrayString verticalDump = getMeasureQueue(VERTICAL).dump();
int length = verticalDump.length();
for (int i = 0; i < length; i++) {
String connectorId = verticalDump.get(i);
if (!horizontalQueue.contains(connectorId)) {
measureTargets.push(connectorId);
}
}
return measureTargets;
}
public void logDependencyStatus(ComponentConnector connector) {
VConsole.log("====");
String connectorId = connector.getConnectorId();
VConsole.log(getDependency(connectorId, HORIZONTAL).toString());
VConsole.log(getDependency(connectorId, VERTICAL).toString());
}
public boolean noMoreChangesExpected(ComponentConnector connector) {
return getDependency(connector.getConnectorId(), HORIZONTAL)
.noMoreChangesExpected()
&& getDependency(connector.getConnectorId(), VERTICAL)
.noMoreChangesExpected();
}
public ComponentConnector getScrollingBoundary(ComponentConnector connector) {
LayoutDependency dependency = getDependency(connector.getConnectorId(),
HORIZONTAL);
if (!dependency.scrollingParentCached) {
ServerConnector parent = dependency.connector.getParent();
if (parent instanceof MayScrollChildren) {
dependency.scrollingBoundary = connector;
} else if (parent instanceof ComponentConnector) {
dependency.scrollingBoundary = getScrollingBoundary((ComponentConnector) parent);
} else {
// No scrolling parent
}
dependency.scrollingParentCached = true;
}
return dependency.scrollingBoundary;
}
}