/*
* Copyright 2000-2021 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.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import org.jsoup.nodes.Element;
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.event.HasUserOriginated;
import com.vaadin.event.MouseEvents.ClickEvent;
import com.vaadin.server.SizeWithUnit;
import com.vaadin.server.Sizeable;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.Registration;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.util.ReflectTools;
/**
* AbstractSplitPanel.
*
* AbstractSplitPanel
is base class for a component container that
* can contain two components. The components are split by a divider element.
*
* @author Vaadin Ltd.
* @since 6.5
*/
public abstract class AbstractSplitPanel extends AbstractComponentContainer {
// TODO use Unit in AbstractSplitPanelState and remove these
private Unit posUnit;
private Unit posMinUnit;
private Unit posMaxUnit;
private final AbstractSplitPanelRpc rpc = new AbstractSplitPanelRpc() {
@Override
public void splitterClick(MouseEventDetails mouseDetails) {
fireEvent(new SplitterClickEvent(AbstractSplitPanel.this,
mouseDetails));
}
@Override
public void setSplitterPosition(float position) {
float oldPosition = getSplitPosition();
getSplitterState().position = position;
fireEvent(new SplitPositionChangeEvent(AbstractSplitPanel.this,
true, oldPosition, getSplitPositionUnit(), position,
getSplitPositionUnit()));
}
};
public AbstractSplitPanel() {
registerRpc(rpc);
setSplitPosition(50, Unit.PERCENTAGE, false);
setSplitPositionLimits(0, Unit.PERCENTAGE, 100, Unit.PERCENTAGE);
}
/**
* Modifiable and Serializable Iterator for the components, used by
* {@link AbstractSplitPanel#getComponentIterator()}.
*/
private class ComponentIterator
implements Iterator, Serializable {
int i = 0;
@Override
public boolean hasNext() {
if (i < getComponentCount()) {
return true;
}
return false;
}
@Override
public Component next() {
if (!hasNext()) {
return null;
}
i++;
if (i == 1) {
return (getFirstComponent() == null ? getSecondComponent()
: getFirstComponent());
} else if (i == 2) {
return getSecondComponent();
}
return null;
}
@Override
public void remove() {
if (i == 1) {
if (getFirstComponent() != null) {
setFirstComponent(null);
i = 0;
} else {
setSecondComponent(null);
}
} else if (i == 2) {
setSecondComponent(null);
}
}
}
/**
* Add a component into this container. The component is added to the right
* or under the previous component.
*
* @param c
* the component to be added.
*/
@Override
public void addComponent(Component c) {
if (getFirstComponent() == null) {
setFirstComponent(c);
} else if (getSecondComponent() == null) {
setSecondComponent(c);
} else {
throw new UnsupportedOperationException(
"Split panel can contain only two components");
}
}
/**
* Sets the first component of this split panel. Depending on the direction
* the first component is shown at the top or to the left.
*
* @param c
* The component to use as first component
*/
public void setFirstComponent(Component c) {
if (getFirstComponent() == c) {
// Nothing to do
return;
}
if (getFirstComponent() != null) {
// detach old
removeComponent(getFirstComponent());
}
getState().firstChild = c;
if (c != null) {
super.addComponent(c);
}
}
/**
* Sets the second component of this split panel. Depending on the direction
* the second component is shown at the bottom or to the right.
*
* @param c
* The component to use as second component
*/
public void setSecondComponent(Component c) {
if (getSecondComponent() == c) {
// Nothing to do
return;
}
if (getSecondComponent() != null) {
// detach old
removeComponent(getSecondComponent());
}
getState().secondChild = c;
if (c != null) {
super.addComponent(c);
}
}
/**
* Gets the first component of this split panel. Depending on the direction
* this is either the component shown at the top or to the left.
*
* @return the first component of this split panel
*/
public Component getFirstComponent() {
return (Component) getState(false).firstChild;
}
/**
* Gets the second component of this split panel. Depending on the direction
* this is either the component shown at the top or to the left.
*
* @return the second component of this split panel
*/
public Component getSecondComponent() {
return (Component) getState(false).secondChild;
}
/**
* Removes the component from this container.
*
* @param c
* the component to be removed.
*/
@Override
public void removeComponent(Component c) {
super.removeComponent(c);
if (c == getFirstComponent()) {
getState().firstChild = null;
} else if (c == getSecondComponent()) {
getState().secondChild = null;
}
}
/**
* Gets an iterator to the collection of contained components. Using this
* iterator it is possible to step through all components contained in this
* container and remove components from it.
*
* @return the component iterator.
*/
@Override
public Iterator iterator() {
return new ComponentIterator();
}
/**
* Gets the number of contained components. Consistent with the iterator
* returned by {@link #getComponentIterator()}.
*
* @return the number of contained components (zero, one or two)
*/
@Override
public int getComponentCount() {
int count = 0;
if (getFirstComponent() != null) {
count++;
}
if (getSecondComponent() != null) {
count++;
}
return count;
}
/* Documented in superclass */
@Override
public void replaceComponent(Component oldComponent,
Component newComponent) {
if (oldComponent == getFirstComponent()) {
setFirstComponent(newComponent);
} else if (oldComponent == getSecondComponent()) {
setSecondComponent(newComponent);
}
}
/**
* Moves the position of the splitter.
*
* @param pos
* the new size of the first region in the unit that was last
* used (default is percentage). Fractions are only allowed when
* unit is percentage.
*/
public void setSplitPosition(float pos) {
setSplitPosition(pos, posUnit, false);
}
/**
* Moves the position of the splitter.
*
* @param pos
* the new size of the region in the unit that was last used
* (default is percentage). Fractions are only allowed when unit
* is percentage.
*
* @param reverse
* if set to true the split splitter position is measured by the
* second region else it is measured by the first region
*/
public void setSplitPosition(float pos, boolean reverse) {
setSplitPosition(pos, posUnit, reverse);
}
/**
* Moves the position of the splitter with given position and unit.
*
* @param pos
* the new size of the first region. Fractions are only allowed
* when unit is percentage.
* @param unit
* the unit (from {@link Sizeable}) in which the size is given.
*/
public void setSplitPosition(float pos, Unit unit) {
setSplitPosition(pos, unit, false);
}
/**
* Moves the position of the splitter with given position and unit.
*
* @param pos
* the new size of the first region. Fractions are only allowed
* when unit is percentage.
* @param unit
* the unit (from {@link Sizeable}) in which the size is given.
* @param reverse
* if set to true the split splitter position is measured by the
* second region else it is measured by the first region
*
*/
public void setSplitPosition(float pos, Unit unit, boolean reverse) {
if (unit != Unit.PERCENTAGE && unit != Unit.PIXELS) {
throw new IllegalArgumentException(
"Only percentage and pixel units are allowed");
}
if (unit != Unit.PERCENTAGE) {
pos = Math.round(pos);
}
float oldPosition = getSplitPosition();
Unit oldUnit = getSplitPositionUnit();
SplitterState splitterState = getSplitterState();
splitterState.position = pos;
splitterState.positionUnit = unit.getSymbol();
splitterState.positionReversed = reverse;
posUnit = unit;
fireEvent(new SplitPositionChangeEvent(AbstractSplitPanel.this, false,
oldPosition, oldUnit, pos, posUnit));
}
/**
* Returns the current position of the splitter, in
* {@link #getSplitPositionUnit()} units.
*
* @return position of the splitter
*/
public float getSplitPosition() {
return getSplitterState(false).position;
}
/**
* Returns the unit of position of the splitter.
*
* @return unit of position of the splitter
* @see #setSplitPosition(float, Unit)
*/
public Unit getSplitPositionUnit() {
return posUnit;
}
/**
* Is the split position reversed. By default the split position is measured
* by the first region, but if split position is reversed the measuring is
* done by the second region instead.
*
* @since 7.3.6
* @return {@code true} if reversed, {@code false} otherwise.
* @see #setSplitPosition(float, boolean)
*/
public boolean isSplitPositionReversed() {
return getSplitterState(false).positionReversed;
}
/**
* Sets the minimum split position to the given position and unit. If the
* split position is reversed, maximum and minimum are also reversed.
*
* @param pos
* the minimum position of the split
* @param unit
* the unit (from {@link Sizeable}) in which the size is given.
* Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS
*/
public void setMinSplitPosition(float pos, Unit unit) {
setSplitPositionLimits(pos, unit, getSplitterState(false).maxPosition,
posMaxUnit);
}
/**
* Returns the current minimum position of the splitter, in
* {@link #getMinSplitPositionUnit()} units.
*
* @return the minimum position of the splitter
*/
public float getMinSplitPosition() {
return getSplitterState(false).minPosition;
}
/**
* Returns the unit of the minimum position of the splitter.
*
* @return the unit of the minimum position of the splitter
*/
public Unit getMinSplitPositionUnit() {
return posMinUnit;
}
/**
* Sets the maximum split position to the given position and unit. If the
* split position is reversed, maximum and minimum are also reversed.
*
* @param pos
* the maximum position of the split
* @param unit
* the unit (from {@link Sizeable}) in which the size is given.
* Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS
*/
public void setMaxSplitPosition(float pos, Unit unit) {
setSplitPositionLimits(getSplitterState(false).minPosition, posMinUnit,
pos, unit);
}
/**
* Returns the current maximum position of the splitter, in
* {@link #getMaxSplitPositionUnit()} units.
*
* @return the maximum position of the splitter
*/
public float getMaxSplitPosition() {
return getSplitterState(false).maxPosition;
}
/**
* Returns the unit of the maximum position of the splitter.
*
* @return the unit of the maximum position of the splitter
*/
public Unit getMaxSplitPositionUnit() {
return posMaxUnit;
}
/**
* Sets the maximum and minimum position of the splitter. If the split
* position is reversed, maximum and minimum are also reversed.
*
* @param minPos
* the new minimum position
* @param minPosUnit
* the unit (from {@link Sizeable}) in which the minimum position
* is given.
* @param maxPos
* the new maximum position
* @param maxPosUnit
* the unit (from {@link Sizeable}) in which the maximum position
* is given.
*/
private void setSplitPositionLimits(float minPos, Unit minPosUnit,
float maxPos, Unit maxPosUnit) {
if ((minPosUnit != Unit.PERCENTAGE && minPosUnit != Unit.PIXELS)
|| (maxPosUnit != Unit.PERCENTAGE
&& maxPosUnit != Unit.PIXELS)) {
throw new IllegalArgumentException(
"Only percentage and pixel units are allowed");
}
SplitterState state = getSplitterState();
state.minPosition = minPos;
state.minPositionUnit = minPosUnit.getSymbol();
posMinUnit = minPosUnit;
state.maxPosition = maxPos;
state.maxPositionUnit = maxPosUnit.getSymbol();
posMaxUnit = maxPosUnit;
}
/**
* Lock the SplitPanels position, disabling the user from dragging the split
* handle.
*
* @param locked
* Set true
if locked, false
otherwise.
*/
public void setLocked(boolean locked) {
getSplitterState().locked = locked;
}
/**
* Is the SplitPanel handle locked (user not allowed to change split
* position by dragging).
*
* @return true
if locked, false
otherwise.
*/
public boolean isLocked() {
return getSplitterState(false).locked;
}
/**
* SplitterClickListener
interface for listening for
* SplitterClickEvent
fired by a SplitPanel
.
*
* @see SplitterClickEvent
* @since 6.2
*/
@FunctionalInterface
public interface SplitterClickListener extends ConnectorEventListener {
public static final Method clickMethod = ReflectTools.findMethod(
SplitterClickListener.class, "splitterClick",
SplitterClickEvent.class);
/**
* SplitPanel splitter has been clicked.
*
* @param event
* SplitterClickEvent event.
*/
public void splitterClick(SplitterClickEvent event);
}
public static class SplitterClickEvent extends ClickEvent {
public SplitterClickEvent(Component source,
MouseEventDetails mouseEventDetails) {
super(source, mouseEventDetails);
}
}
/**
* Interface for listening for {@link SplitPositionChangeEvent}s fired by a
* SplitPanel.
*
* @since 7.5.0
*/
@FunctionalInterface
public interface SplitPositionChangeListener
extends ConnectorEventListener {
public static final Method moveMethod = ReflectTools.findMethod(
SplitPositionChangeListener.class, "onSplitPositionChanged",
SplitPositionChangeEvent.class);
/**
* SplitPanel splitter position has been changed.
*
* @param event
* SplitPositionChangeEvent event.
*/
public void onSplitPositionChanged(SplitPositionChangeEvent event);
}
/**
* Event that indicates a change in SplitPanel's splitter position.
*
* @since 7.5.0
*/
public static class SplitPositionChangeEvent extends Component.Event
implements HasUserOriginated {
private final float oldPosition;
private final Unit oldUnit;
private final float position;
private final Unit unit;
private final boolean userOriginated;
/**
* Creates a split position change event.
*
* @param source
* split panel from which the event originates
* @param userOriginated
* true if the event is directly based on user actions
* @param oldPosition
* old split position
* @param oldUnit
* old unit of split position
* @param position
* new split position
* @param unit
* new split position unit
* @since 8.1
*/
public SplitPositionChangeEvent(final Component source,
final boolean userOriginated, final float oldPosition,
final Unit oldUnit, final float position, final Unit unit) {
super(source);
this.userOriginated = userOriginated;
this.oldUnit = oldUnit;
this.oldPosition = oldPosition;
this.position = position;
this.unit = unit;
}
/**
* Returns the new split position that triggered this change event.
*
* @return the new value of split position
*/
public float getSplitPosition() {
return position;
}
/**
* Returns the new split position unit that triggered this change event.
*
* @return the new value of split position
*/
public Unit getSplitPositionUnit() {
return unit;
}
/**
* Returns the position of the split before this change event occurred.
*
* @since 8.1
*
* @return the split position previously set to the source of this event
*/
public float getOldSplitPosition() {
return oldPosition;
}
/**
* Returns the position unit of the split before this change event
* occurred.
*
* @since 8.1
*
* @return the split position unit previously set to the source of this
* event
*/
public Unit getOldSplitPositionUnit() {
return oldUnit;
}
/**
* {@inheritDoc}
*
* @since 8.1
*/
@Override
public boolean isUserOriginated() {
return userOriginated;
}
}
public Registration addSplitterClickListener(
SplitterClickListener listener) {
return addListener(EventId.CLICK_EVENT_IDENTIFIER,
SplitterClickEvent.class, listener,
SplitterClickListener.clickMethod);
}
@Deprecated
public void removeSplitterClickListener(SplitterClickListener listener) {
removeListener(EventId.CLICK_EVENT_IDENTIFIER, SplitterClickEvent.class,
listener);
}
/**
* Register a listener to handle {@link SplitPositionChangeEvent}s.
*
* @since 8.0
* @param listener
* {@link SplitPositionChangeListener} to be registered.
*/
public Registration addSplitPositionChangeListener(
SplitPositionChangeListener listener) {
return addListener(SplitPositionChangeEvent.class, listener,
SplitPositionChangeListener.moveMethod);
}
/**
* Removes a {@link SplitPositionChangeListener}.
*
* @since 7.5.0
* @param listener
* SplitPositionChangeListener to be removed.
*/
@Deprecated
public void removeSplitPositionChangeListener(
SplitPositionChangeListener listener) {
removeListener(SplitPositionChangeEvent.class, listener);
}
@Override
protected AbstractSplitPanelState getState() {
return (AbstractSplitPanelState) super.getState();
}
@Override
protected AbstractSplitPanelState getState(boolean markAsDirty) {
return (AbstractSplitPanelState) super.getState(markAsDirty);
}
private SplitterState getSplitterState() {
return ((AbstractSplitPanelState) super.getState()).splitterState;
}
private SplitterState getSplitterState(boolean markAsDirty) {
return ((AbstractSplitPanelState) super.getState(
markAsDirty)).splitterState;
}
/*
* (non-Javadoc)
*
* @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
* com.vaadin.ui.declarative.DesignContext)
*/
@Override
public void readDesign(Element design, DesignContext designContext) {
// handle default attributes
super.readDesign(design, designContext);
// handle custom attributes, use default values if no explicit value
// set
// There is no setter for reversed, so it will be handled using
// setSplitPosition.
boolean reversed = false;
if (design.hasAttr("reversed")) {
reversed = DesignAttributeHandler.readAttribute("reversed",
design.attributes(), Boolean.class);
setSplitPosition(getSplitPosition(), reversed);
}
if (design.hasAttr("split-position")) {
SizeWithUnit splitPosition = SizeWithUnit.parseStringSize(
design.attr("split-position"), Unit.PERCENTAGE);
setSplitPosition(splitPosition.getSize(), splitPosition.getUnit(),
reversed);
}
if (design.hasAttr("min-split-position")) {
SizeWithUnit minSplitPosition = SizeWithUnit.parseStringSize(
design.attr("min-split-position"), Unit.PERCENTAGE);
setMinSplitPosition(minSplitPosition.getSize(),
minSplitPosition.getUnit());
}
if (design.hasAttr("max-split-position")) {
SizeWithUnit maxSplitPosition = SizeWithUnit.parseStringSize(
design.attr("max-split-position"), Unit.PERCENTAGE);
setMaxSplitPosition(maxSplitPosition.getSize(),
maxSplitPosition.getUnit());
}
// handle children
if (design.children().size() > 2) {
throw new DesignException(
"A split panel can contain at most two components.");
}
for (Element childElement : design.children()) {
Component childComponent = designContext.readDesign(childElement);
if (childElement.hasAttr(":second")) {
setSecondComponent(childComponent);
} else {
addComponent(childComponent);
}
}
}
@Override
protected Collection getCustomAttributes() {
Collection attributes = super.getCustomAttributes();
// the setters of the properties do not accept strings such as "20px"
attributes.add("split-position");
attributes.add("min-split-position");
attributes.add("max-split-position");
// no explicit setter for reversed
attributes.add("reversed");
return attributes;
}
@Override
public void writeDesign(Element design, DesignContext designContext) {
// handle default attributes (also clears children and attributes)
super.writeDesign(design, designContext);
// handle custom attributes (write only if a value is not the
// default value)
AbstractSplitPanel def = designContext.getDefaultInstance(this);
if (getSplitPosition() != def.getSplitPosition()
|| !def.getSplitPositionUnit().equals(getSplitPositionUnit())) {
String splitPositionString = asString(getSplitPosition())
+ getSplitPositionUnit();
design.attr("split-position", splitPositionString);
}
if (getMinSplitPosition() != def.getMinSplitPosition() || !def
.getMinSplitPositionUnit().equals(getMinSplitPositionUnit())) {
design.attr("min-split-position", asString(getMinSplitPosition())
+ getMinSplitPositionUnit());
}
if (getMaxSplitPosition() != def.getMaxSplitPosition() || !def
.getMaxSplitPositionUnit().equals(getMaxSplitPositionUnit())) {
design.attr("max-split-position", asString(getMaxSplitPosition())
+ getMaxSplitPositionUnit());
}
if (getSplitterState().positionReversed) {
design.attr("reversed", true);
}
// handle child components
if (!designContext.shouldWriteChildren(this, def)) {
return;
}
Component firstComponent = getFirstComponent();
Component secondComponent = getSecondComponent();
if (firstComponent != null) {
Element childElement = designContext.createElement(firstComponent);
design.appendChild(childElement);
}
if (secondComponent != null) {
Element childElement = designContext.createElement(secondComponent);
if (firstComponent == null) {
childElement.attr(":second", true);
}
design.appendChild(childElement);
}
}
private String asString(float number) {
int truncated = (int) number;
if (truncated == number) {
return "" + truncated;
}
return "" + number;
}
}