com.codename1.ui.Container Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.ui;
import com.codename1.ui.animations.Motion;
import com.codename1.ui.animations.Transition;
import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.layouts.Layout;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.geom.Rectangle;
import com.codename1.impl.CodenameOneImplementation;
import com.codename1.ui.animations.ComponentAnimation;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.LayeredLayout;
import com.codename1.ui.plaf.LookAndFeel;
import com.codename1.ui.plaf.Style;
import java.util.*;
/**
* A composite pattern with {@link Component}, allows nesting and arranging multiple
* components using a pluggable layout manager architecture. Containers can be nested
* one within the other to form elaborate UI's. By default Containers use {@link com.codename1.ui.layouts.FlowLayout}
* which isn't ideal for most use cases.
*
*
* Components within the Container MUST be arranged using a layout manager!
* This allows the UI to adapt to different resolutions, DPI, orientation changes etc. seamlessly. Invoking any
* bounds setting method will produce unpredictable results. To learn about layout managers check out the
* relevant section in the developer guide.
*
*
* A container doesn't implicitly reflow its elements and in that regard follows the direction of AWT/Swing. As
* a result the layout can be animated to create a flowing effect for UI changes. This also provides improved
* performance as a bonus. See this sample of {@code Container} animation:
*
*
*
*
* Many components within Codename One (e.g. {@link com.codename1.ui.tree.Tree},
* {@link com.codename1.ui.table.Table},
* {@link com.codename1.components.MultiButton} etc.) derive from Container instead of Component. This allows
* such components to provide very rich functionality by building on top of the existing functionality.
* Container also provides the lead component functionality that allows treating an entire Container hierarchy
* as a single component. This is discussed in depth within the developer guide.
*
*
* @see com.codename1.ui.layouts
* @see Component
* @author Chen Fishbein
*/
public class Container extends Component implements Iterable{
static boolean enableLayoutOnPaint = true;
// A 2nd flag for enabling layout on paint. In order for layoutOnPaint to occur,
// both the enableLayoutOnPaint and allowEnableLayoutOnPaint flags must be true.
// This flag can be set on any Container (e.g. form), and will cause it to be propagated
// down to its children. So you can set this at the form level, in order to enable this behaviour
// for the whole form.
private boolean allowEnableLayoutOnPaint = false;
private Component leadComponent;
private Layout layout;
private java.util.ArrayList components = new java.util.ArrayList();
/**
* A queue that keeps track of changes to the children while an animation is in progress.
* @see #getChildrenAsList(boolean)
* @see #iterator(boolean)
* @see #insertComponentAt(int, java.lang.Object, com.codename1.ui.Component)
* @see #removeComponentImpl(com.codename1.ui.Component)
*/
private java.util.ArrayList changeQueue= new java.util.ArrayList();
private boolean shouldLayout = true;
boolean scrollableX;
boolean scrollableY;
private java.util.Vector cmpTransitions;
private int scrollIncrement = 20;
private boolean blockFocus = false;
private boolean dontRecurseContainer;
private UIManager uiManager;
private boolean surface;
/**
* Encapsulates a change to the container's children. Used to keep track of
* queued inserts and removes that occur while an animation is in progress.
*/
private static class QueuedChange {
/**
* The component that was inserted or removed.
*/
private final Component component;
/**
* The type of change. Either {@link #TYPE_INSERT} or {@link #TYPE_REMOVE}
*/
private final int type;
/**
* For {@link #type} to indicate an insertion.
*/
static final int TYPE_INSERT=0;
/**
* For {@link #type} to indicate a removal.
*/
static final int TYPE_REMOVE=1;
/**
* Creates a new queued change.
* @param type Either {@link #TYPE_INSERT} or {@link #TYPE_REMOVE}
* @param cmp The component that was inserted or removed.
*/
QueuedChange(int type, Component cmp) {
this.type = type;
this.component = cmp;
}
}
/**
* Encapsulates a child component insertion that occurs during an animation.
*/
private static class QueuedInsertion extends QueuedChange {
/**
* The component constraint of the component that was inserted.
*/
private Object constraint;
/**
* The index where the component should be inserted.
*/
private int index;
/**
* Creates a new queued insertion.
* @param index The index where the component is inserted.
* @param constraint The constraint.
* @param cmp The component that was inserted.
*/
QueuedInsertion(int index, Object constraint, Component cmp) {
super(TYPE_INSERT, cmp);
this.index = index;
this.constraint = constraint;
}
}
/**
* Encapsulates the removal of a component from the children while an animation
* is in progress.
*/
private static class QueuedRemoval extends QueuedChange {
QueuedRemoval(Component cmp) {
super(TYPE_REMOVE, cmp);
}
}
/**
* Workaround for the behavior of the sidemenu bar on iOS etc. which translates aggressively,
* this is visible with the table component where the lines slide out of place
*/
static int sidemenuBarTranslation;
/**
* Constructs a new Container with a new layout manager and UIID
*
* @param layout the specified layout manager
* @param uiid the uiid of the container
*/
public Container(Layout layout, String uiid) {
super();
setUIID(uiid);
this.layout = layout;
setFocusable(false);
}
/**
* Constructs a new Container with a new layout manager.
*
* @param layout the specified layout manager
*/
public Container(Layout layout) {
this(layout, "Container");
}
/**
* Constructs a new Container, with a {@link FlowLayout}.
*/
public Container() {
this(new FlowLayout());
}
/**
* Short-hand for enclosing a component within a Container
* @param l the layout
* @param cmp the component to enclose
* @param cons the constraint for the component
* @return a newly created container containing the given component
*/
public static Container encloseIn(Layout l, Component cmp, Object cons) {
Container cnt = new Container(l);
if(cons instanceof Component) {
// this got sent to the wong method by dumb compiler...
return cnt.add(cmp).add((Component)cons);
}
if(cons != null) {
cnt.addComponent(cons, cmp);
} else {
cnt.addComponent(cmp);
}
return cnt;
}
/**
* Short-hand for enclosing multiple components in a container typically a box layout
* @param l the layout
* @param cmp the components to enclose
* @return a newly created container containing the given components
*/
public static Container encloseIn(Layout l, Component... cmp) {
Container cnt = new Container(l);
for(Component c : cmp) {
cnt.addComponent(c);
}
return cnt;
}
/**
*
* {@inheritDoc}
*/
protected void initLaf(UIManager uim) {
if(uim == getUIManager() && isInitialized()){
return;
}
super.initLaf(uim);
LookAndFeel laf = uim.getLookAndFeel();
setSmoothScrolling(laf.isDefaultSmoothScrolling());
if(components != null){
int count = getComponentCount();
for (int i = 0; i < count; i++) {
Component c = getComponentAt(i);
c.initLaf(uim);
}
}
}
/**
* {@inheritDoc}
*/
public UIManager getUIManager() {
if(uiManager != null) {
return uiManager;
}
return super.getUIManager();
}
/**
* An atomic operation that wraps the current component in a Container with
* a layered layout. This prevents us from having to initialize and deinitialize
* all of the components in a sub-tree because we want to re-root it. In particular
* Form.getLayeredPane() re-roots the entire content pane the first time it is
* called on a form. If the form contains native peers there is a flicker which
* is quite annoying. Providing a way to do this atomically results in a better
* user experience.
* @return The Container that is the new parent of this component.
*/
Container wrapInLayeredPane() {
final Container oldParent = getParent();
final Container newParent = new Container(new LayeredLayout());
final Layout parentLayout = oldParent != null && oldParent.layout != null ? oldParent.layout : null;
final Object constraint = parentLayout != null ? parentLayout.getComponentConstraint(this) : null;
newParent.setParent(oldParent);
newParent.components.add(this);
final Runnable r = new Runnable() {
public void run() {
if (parentLayout != null) {
parentLayout.removeLayoutComponent(Container.this);
parentLayout.addLayoutComponent(constraint, newParent, oldParent);
}
newParent.initComponentImpl();
if (oldParent != null) {
int cmpIndex = -1;
for (int i=0; i toProcess = new ArrayList(elevatedComponents);
elevatedComponents.clear();
for (Component elevated : toProcess) {
((Component)elevated).registerElevatedInternal(elevated);
}
}
} else {
// We are now a surface. See if there are any projections against parent the parent
// surface that this should intercept
Container parentSurface = findSurface();
if (parentSurface != null) {
if (parentSurface.elevatedComponents != null && !parentSurface.elevatedComponents.isEmpty()) {
ArrayList toProcess = new ArrayList(parentSurface.elevatedComponents);
for (Component elevated : toProcess) {
if (contains(elevated)) {
// This component is actually inside us, so it should project on
// us now.
((Component)elevated).registerElevatedInternal(elevated);
}
}
}
}
}
}
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param cmp the component to add
* @return this for call chaining
*/
public Container add(Component cmp) {
addComponent(cmp);
return this;
}
/**
* Identical to add(x).add(y) only with a shorter syntax
* @param cmps the other components to add
* @return this for call chaining
*/
public Container addAll(Component... cmps) {
for(Component c : cmps) {
addComponent(c);
}
return this;
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param constraint the layout constraint if applicable
* @param cmp the component to add
* @return this for call chaining
*/
public Container add(Object constraint, Component cmp) {
addComponent(constraint, cmp);
return this;
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param label a string that will be wrapped as a label, this is equivalent to calling add(new Label(l))
* @return this for call chaining
*/
public Container add(String label) {
return add(new Label(label));
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param img an image that will be wrapped as a label, this is equivalent to calling add(new Label(l))
* @return this for call chaining
*/
public Container add(Image img) {
return add(new Label(img));
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param constraint the layout constraint if applicable
* @param label a component that will be wrapped as a label, this is equivalent to calling add(new Label(l))
* @return this for call chaining
*/
public Container add(Object constraint, String label) {
return add(constraint, new Label(label));
}
/**
* Simpler version of addComponent that allows chaining the calls for shorter syntax
* @param constraint the layout constraint if applicable
* @param img an image that will be wrapped as a label, this is equivalent to calling add(new Label(l))
* @return this for call chaining
*/
public Container add(Object constraint, Image img) {
return add(constraint, new Label(img));
}
/**
* Allows replacing the UIManager in a component hierarchy to update the look and feel
* only to a specific hierarchy
* @param uiManager UIManager instance
*/
public void setUIManager(UIManager uiManager) {
this.uiManager = uiManager;
}
/**
* Sets the lead component for this container, a lead component takes over the entire
* component hierarchy and receives all the events for the container hierarchy.
*
* @param lead component that takes over the hierarchy
*/
public void setLeadComponent(Component lead) {
if (lead == leadComponent) {
return;
}
leadComponent = lead;
if(lead == null) {
// clear the lead component from the hierarchy
if (!isBlockLead() && getParent() != null && getParent().hasLead) {
// hasLead should still be true because of parent lead
} else {
setFocusable(false);
hasLead = false;
if (isInitialized()) {
enableFocusAndDeinitLead(this);
}
}
} else {
if(isInitialized()) {
initLead();
}
}
}
void focusGainedInternal() {
super.focusGainedInternal();
if(leadComponent != null) {
setFocusLead(true);
}
}
void focusLostInternal() {
super.focusLostInternal();
if(leadComponent != null) {
setFocusLead(false);
}
}
/**
* Returns the lead component for this hierarchy if such a component is defined
*
* @return the lead component
*/
public Component getLeadComponent() {
if(leadComponent != null) {
return leadComponent;
}
if(isBlockLead()) {
return null;
}
if(hasLead) {
return super.getLeadComponent();
}
return null;
}
/**
* Returns the lead container thats handling the leading, this is useful for
* a container hierarchy where the parent container might not be the leader
*
* @return the lead component
*/
public Container getLeadParent() {
if(leadComponent != null) {
return this;
}
if(isBlockLead()) {
return null;
}
if(hasLead) {
return getParent().getLeadParent();
}
return null;
}
private void initLead() {
disableFocusAndInitLead(this);
setFocusable(true);
hasLead = leadComponent != null || !isBlockLead();
}
/**
* {@inheritDoc}
*/
public void keyPressed(int k) {
if(leadComponent != null) {
leadComponent.keyPressed(k);
repaint();
}
}
/**
* {@inheritDoc}
*/
public void keyReleased(int k) {
if(leadComponent != null) {
leadComponent.keyReleased(k);
repaint();
}
}
private void disableFocusAndInitLead(Container c) {
for(int iter = 0 ; iter < c.getComponentCount() ; iter++) {
Component cu = c.getComponentAt(iter);
boolean isContainer = (cu instanceof Container);
if (!cu.isBlockLead()) {
cu.setFocusable(false);
}
if (isContainer) {
cu.hasLead = ((Container)cu).leadComponent != null || !cu.isBlockLead();
} else {
cu.hasLead = !cu.isBlockLead();
}
if(isContainer && cu.hasLead) {
disableFocusAndInitLead((Container)cu);
if (((Container)cu).leadComponent != null) {
((Container)cu).setFocusable(true);
}
}
}
}
private void enableFocusAndDeinitLead(Container c) {
for(int iter = 0 ; iter < c.getComponentCount() ; iter++) {
Component cu = c.getComponentAt(iter);
boolean isContainer = (cu instanceof Container);
if (isContainer) {
cu.hasLead = ((Container)cu).leadComponent != null;
} else {
cu.hasLead = false;
}
if(isContainer && !cu.hasLead) {
enableFocusAndDeinitLead((Container)cu);
}
if (!cu.hasLead) {
cu.resetFocusable();
}
}
}
/**
* Returns the layout manager responsible for arranging this container.
*
* @return the container layout manager
*/
public Layout getLayout() {
return layout;
}
/**
* Returns the actual layout of this container. For most components this just
* wraps {@link #getLayout()}, but some classes (e.g. Form) don't return their
* *actual* layout. In such cases, this method will return the component's *actual*
* layout.
* @return
*/
final Layout getActualLayout() {
return layout;
}
/**
* Sets the layout manager responsible for arranging this container
*
* @param layout the specified layout manager
*/
public void setLayout(Layout layout) {
if(layout.isConstraintTracking()) {
for(int iter = 0 ; iter < getComponentCount() ; iter++) {
Component c = getComponentAt(iter);
Object cons = this.layout.getComponentConstraint(c);
if(cons != null) {
layout.addLayoutComponent(cons, c, this);
}
}
}
this.layout = layout;
if(layout instanceof BorderLayout && isScrollable()) {
setScrollable(false);
}
}
/**
* Same as setShouldCalcPreferredSize(true) but made accessible for
* layout managers
*/
public void invalidate() {
setShouldCalcPreferredSize(true);
}
/**
* Flags this container to preform layout
*
* @param layout
*/
protected void setShouldLayout(boolean layout) {
if (!shouldCalcScrollSize) {
this.shouldCalcScrollSize = layout;
}
if (shouldLayout != layout) {
shouldLayout = layout;
shouldCalcPreferredSize = layout;
shouldCalcScrollSize = layout;
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
if(cmp instanceof Container){
cmp.setShouldCalcPreferredSize(shouldCalcPreferredSize);
}
}
Container parent = getParent();
if(parent != null){
parent.setShouldLayout(layout);
}
}
}
/**
* {@inheritDoc}
*/
public void setShouldCalcPreferredSize(boolean shouldCalcPreferredSize) {
// minor optimization preventing repeated invokations to setShouldCalcPreferredSize
if(shouldCalcPreferredSize && this.shouldLayout && this.shouldCalcPreferredSize && !isInitialized()) {
Container p = getParent();
if(p != null && p.shouldLayout && p.shouldCalcPreferredSize) {
return;
}
}
super.setShouldCalcPreferredSize(shouldCalcPreferredSize);
shouldLayout = shouldCalcPreferredSize;
if (shouldLayout) {
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
if (cmp instanceof Container) {
((Container) cmp).setShouldCalcPreferredSize(shouldCalcPreferredSize);
}
}
}
}
/**
* Returns the width for layout manager purposes, this takes scrolling
* into consideration unlike the getWidth method.
*
* @return the layout width
*/
public int getLayoutWidth() {
if (scrollableX) {
return Math.max(getWidth(), getPreferredW());
} else {
Container parent = getScrollableParentX();
if (parent != null && parent.scrollableX) {
return Math.max(getWidth(), getPreferredW());
}
int width = getWidth();
if (width <= 0) {
return getPreferredW();
}
return width;
}
}
/**
* Returns the height for layout manager purposes, this takes scrolling
* into consideration unlike the getHeight method.
*
* @return the layout height
*/
public int getLayoutHeight() {
if (scrollableY) {
return Math.max(getHeight(), getPreferredH());
} else {
Container parent = getScrollableParentY();
if (parent != null && parent.scrollableY) {
return Math.max(getHeight(), getPreferredH());
}
int height = getHeight();
if (height <= 1) {
return getPreferredH();
}
return height;
}
}
/**
* Invokes apply/setRTL recursively on all the children components of this container
*
* @param rtl right to left bidi indication
* @see Component#setRTL(boolean)
*/
public void applyRTL(boolean rtl) {
setRTL(rtl);
int c = getComponentCount();
for(int iter = 0 ; iter < c ; iter++) {
Component current = getComponentAt(iter);
if(current instanceof Container) {
((Container)current).applyRTL(rtl);
} else {
current.setRTL(rtl);
}
}
}
/**
* Returns a parent container that is scrollableX or null if no parent is
* scrollable.
*
* NOTE: This is a utility method that is designed for the getLayoutWidth()
* method, which is why it obeys the constrainHeightWhenScrollable() attribute.
*
* @return a parent container that is scrollable or null if no parent is
* scrollable.
*/
private Container getScrollableParentX() {
Container parent = getParent();
while (parent != null) {
if (parent.scrollableX && !parent.constrainWidthWhenScrollable()) {
return parent;
}
if (parent.hasFixedPreferredSize()) {
return parent;
}
parent = parent.getParent();
}
return null;
}
/**
* Returns a parent container that is scrollableY or null if no parent is
* scrollable.
*
* NOTE: This is a utility method that is designed for the getLayoutHeight()
* method, which is why it obeys the constrainHeightWhenScrollable() attribute.
*
* @return a parent container that is scrollable or null if no parent is
* scrollable.
*/
private Container getScrollableParentY() {
Container parent = getParent();
while (parent != null) {
if (parent.scrollableY && !parent.constrainHeightWhenScrollable()) {
return parent;
}
if (parent.hasFixedPreferredSize()) {
return parent;
}
parent = parent.getParent();
}
return null;
}
/**
* Indicates that children's widths should be calculated as if this component weren't
* scrollable-X, even when the component is scrollable X. Normally, when a component
* is figuring out its layout width, it will walk up the UI hierarchy to find the
* first scrollable container. If there is a scrollable container, then the component
* will try to grow as big as it wants. If there are no scrollable containers found,
* it will constrain itself to the space available. In some cases, we may want the children
* of a component to lay themselves out conservatively though because it wants to use its
* scrollability for other features.
* @return True if children should calculate their layout widgets as if the component
* weren't scrollable.
* @since 7.0
*/
protected boolean constrainWidthWhenScrollable() {
return false;
}
/**
* Indicates that children's widths should be calculated as if this component weren't
* scrollable-X, even when the component is scrollable Y. Normally, when a component
* is figuring out its layout width, it will walk up the UI hierarchy to find the
* first scrollable container. If there is a scrollable container, then the component
* will try to grow as big as it wants. If there are no scrollable containers found,
* it will constrain itself to the space available. In some cases, we may want the children
* of a component to lay themselves out conservatively though because it wants to use its
* scrollability for other features.
* @return True if children should calculate their layout widgets as if the component
* weren't scrollable.
* @since 7.0
*/
protected boolean constrainHeightWhenScrollable() {
return false;
}
/**
* Adds a Component to the Container
*
* @param cmp the component to be added
*/
public void addComponent(Component cmp) {
layout.addLayoutComponent(null, cmp, this);
insertComponentAt(Integer.MAX_VALUE, null, cmp);
}
/**
* Adds a Component to the Container
*
* @param constraints this method is useful when the Layout requires a constraint
* such as the BorderLayout.
* In this case you need to specify an additional data when you add a Component,
* such as "CENTER", "NORTH"...
*
* @param cmp component to add
*/
public void addComponent(final Object constraints, final Component cmp) {
layout.addLayoutComponent(constraints, cmp, this);
insertComponentAt(Integer.MAX_VALUE, null, cmp);
}
/**
* Adds a Component to the Container
*
* @param index location to insert the Component
* @param constraints this method is useful when the Layout requires a constraint
* such as the BorderLayout.
* In this case you need to specify an additional data when you add a Component,
* such as "CENTER", "NORTH"...
* @param cmp component to add
*/
public void addComponent(int index, Object constraints, Component cmp) {
insertComponentAt(index, constraints, cmp);
}
void insertComponentAt(final int index, final Object constraint, final Component cmp) {
final AnimationManager a = getAnimationManager();
if(a != null && a.isAnimating()) {
// pretend like the component was already added
if(cmp.getParent() != null) {
throw new IllegalArgumentException("Component is already contained in Container: " + cmp.getParent());
}
cmp.setParent(this);
final QueuedInsertion insertion = new QueuedInsertion(index, constraint, cmp);
changeQueue.add(insertion);
a.addAnimation(new ComponentAnimation() {
private boolean alreadyAdded;
@Override
public boolean isInProgress() {
return false;
}
@Override
protected void updateState() {
if(!alreadyAdded) {
try {
alreadyAdded = true;
cmp.setParent(null);
if(constraint != null) {
layout.addLayoutComponent(constraint, cmp, Container.this);
}
insertComponentAtImpl(index, cmp);
} finally {
changeQueue.remove(insertion);
}
revalidateLater();
}
}
@Override
public void flush() {
updateState();
}
});
} else {
if(constraint != null) {
layout.addLayoutComponent(constraint, cmp, this);
}
insertComponentAtImpl(index, cmp);
}
}
void insertComponentAtImpl(int index, final Component cmp) {
if(index == Integer.MAX_VALUE) {
index = components.size();
}
if (cmp.getParent() != null) {
throw new IllegalArgumentException("Component is already contained in Container: " + cmp.getParent());
}
if(cmp instanceof Form) {
cmp.setVisible(true);
cmp.setPreferredSize(null);
}
UIManager manager = getUIManager();
boolean refreshLaf = manager != cmp.getUIManager();
cmp.setParent(this);
if(refreshLaf){
Display.getInstance().callSerially(new Runnable() {
public void run() {
cmp.refreshTheme(false);
}
});
}
components.add(index, cmp);
if (layout instanceof BorderLayout && !BorderLayout.OVERLAY.equals(layout.getComponentConstraint(cmp))) {
// Make sure overlay component is always on top
Component overlay = ((BorderLayout)layout).getOverlay();
if (overlay != null) {
components.remove(overlay);
components.add(index, overlay);
}
}
setShouldCalcPreferredSize(true);
if (isInitialized()) {
cmp.initComponentImpl();
}
}
/**
* This method adds the Component at a specific index location in the Container
* Components array.
*
* @param index location to insert the Component
* @param cmp the Component to add
* @throws ArrayIndexOutOfBoundsException if index is out of bounds
* @throws IllegalArgumentException if Component is already contained or
* the cmp is a Form Component
*/
public void addComponent(int index, Component cmp) {
insertComponentAt(index, null, cmp);
}
/**
* This method replaces the current Component with the next Component.
* Current Component must be contained in this Container.
* This method returns when transition has finished.
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
*/
public void replaceAndWait(final Component current, final Component next, final Transition t) {
replaceComponents(current, next, t, true, false, null, 0, 0, true);
}
/**
* This method replaces the current Component with the next Component.
* Current Component must be contained in this Container.
* This method returns when transition has finished.
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
* @param layoutAnimationSpeed the speed of the layout animation after replace is completed
*/
public void replaceAndWait(final Component current, final Component next, final Transition t, int layoutAnimationSpeed) {
enableLayoutOnPaint = false;
replaceComponents(current, next, t, true, false, null, 0, layoutAnimationSpeed, true);
if(layoutAnimationSpeed > 0) {
animateLayoutAndWait(layoutAnimationSpeed);
}
dontRecurseContainer = false;
enableLayoutOnPaint = true;
}
/**
* This method replaces the current Component with the next Component
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
* @param onFinish invoked when the replace operation is completed, may be null
* @param growSpeed after replace is completed the component can gradually grow/shrink to fill up
* available room, set this to 0 for immediate growth or any larger number for gradual animation. -1 indicates
* a special case where no validation occurs
*/
public void replace(final Component current, final Component next, final Transition t, Runnable onFinish, int growSpeed) {
replaceComponents(current, next, t, false, false, onFinish, growSpeed, 0, true);
}
/**
* This method replaces the current Component with the next Component.
* Current Component must be contained in this Container.
* This method returns when transition has finished.
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
* @param dropEvents indicates if the display should drop all events
* while this Component replacing is happening
*/
public void replaceAndWait(final Component current, final Component next,
final Transition t, boolean dropEvents) {
replaceComponents(current, next, t, true, dropEvents, null, 0, 0, true);
}
/**
* This method replaces the current Component with the next Component.
* Current Component must be contained in this Container.
* This method return immediately.
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
*/
public void replace(final Component current, final Component next, final Transition t) {
replaceComponents(current, next, t, false, false, null, 0, 0, true);
}
/**
* This method creates an animation component that replaces the current Component with the next Component.
* Current Component must be contained in this Container.
* This method return immediately.
*
* @param current a Component to remove from the Container
* @param next a Component that replaces the current Component
* @param t a Transition between the add and removal of the Components
* a Transition can be null
* @return animation component that can be queued
*/
public ComponentAnimation createReplaceTransition(Component current, Component next, Transition t) {
return replaceComponents(current, next, t, false, false, null, 0, 0, false);
}
private ComponentAnimation replaceComponents(final Component current, final Component next,
final Transition t, boolean wait, boolean dropEvents, Runnable onFinish, int growSpeed, int layoutAnimationSpeed,
boolean addAnimtion) {
if (!contains(current)) {
throw new IllegalArgumentException("Component " + current + " is not contained in this Container");
}
if (t == null || !isVisible() || getComponentForm() == null) {
next.setX(current.getX());
next.setY(current.getY());
next.setWidth(current.getWidth());
next.setHeight(current.getHeight());
replace(current, next, false);
return null;
}
setScrollX(0);
setScrollY(0);
next.setX(current.getX());
next.setY(current.getY());
next.setWidth(current.getWidth());
next.setHeight(current.getHeight());
next.setParent(this);
if (next instanceof Container) {
((Container) next).layoutContainer();
}
final TransitionAnimation anim = new TransitionAnimation(this, current, next, t);
anim.growSpeed = growSpeed;
anim.layoutAnimationSpeed = layoutAnimationSpeed;
// register the transition animation
/*getComponentForm().registerAnimatedInternal(anim);
//wait until animation has finished
if (wait) {
Display.getInstance().invokeAndBlock(anim, dropEvents);
}*/
if(addAnimtion) {
if(wait) {
getAnimationManager().addAnimationAndBlock(anim);
} else {
if(onFinish != null) {
getAnimationManager().addUIMutation(this, anim, onFinish);
} else {
getAnimationManager().addUIMutation(this, anim);
}
}
}
return anim;
}
private boolean isParentOf(Component c) {
c = c.getParent();
if (c == null || c instanceof Form) {
return false;
}
return (c == this) || isParentOf(c);
}
void onParentPositionChange() {
int cmpCount = getComponentCount();
for (int iter = 0; iter < cmpCount ; iter++) {
Component c = getComponentAt(iter);
c.onParentPositionChange();
}
}
@Override
boolean onOrientationChange() {
boolean v = super.onOrientationChange();
int cmpCount = getComponentCount();
for (int iter = 0; iter < cmpCount ; iter++) {
Component c = getComponentAt(iter);
v = c.onOrientationChange() || v;
}
return v;
}
private boolean requestFocusChild(boolean avoidRepaint) {
int cmpCount = getComponentCount();
for (int iter = 0; iter < cmpCount ; iter++) {
Component c = getComponentAt(iter);
if (c.isFocusable()) {
if(avoidRepaint) {
getComponentForm().setFocusedInternal(c);
} else {
c.requestFocus();
}
return true;
}
if (c instanceof Container && ((Container) c).requestFocusChild(avoidRepaint)) {
return true;
}
}
return false;
}
private void cancelRepaintsRecursively(Component c, CodenameOneImplementation l) {
if(c instanceof Container) {
Container cnt = (Container)c;
int count = cnt.getComponentCount();
for(int i = 0 ; i < count ; i++) {
cancelRepaintsRecursively(cnt.getComponentAt(i), l);
}
}
l.cancelRepaint(c);
}
private void cancelRepaintsRecursively(Component c) {
cancelRepaintsRecursively(c, Display.impl);
}
void replace(final Component current, final Component next, boolean avoidRepaint) {
int index = components.indexOf(current);
boolean currentFocused = false;
if (current.getComponentForm() != null) {
Component currentF = current.getComponentForm().getFocused();
currentFocused = currentF == current;
if (!currentFocused && current instanceof Container && currentF != null && ((Container) current).isParentOf(currentF)) {
currentFocused = true;
}
}
Object constraint = layout.getComponentConstraint(current);
if (constraint != null) {
removeComponentImplNoAnimationSafety(current);
layout.addLayoutComponent(constraint, next, Container.this);
} else {
removeComponentImplNoAnimationSafety(current);
}
cancelRepaintsRecursively(current);
next.setParent(null);
if (index < 0) {
index = 0;
}
insertComponentAtImpl(index, next);
if (currentFocused) {
if (next.isFocusable()) {
if(avoidRepaint) {
getComponentForm().setFocusedInternal(next);
} else {
next.requestFocus();
}
} else {
if (next instanceof Container) {
((Container) next).requestFocusChild(avoidRepaint);
}
}
}
}
/**
* {@inheritDoc}
*/
void initComponentImpl() {
if (!isInitialized()) {
super.initComponentImpl();
}
Container p = getParent();
if (p != null) {
allowEnableLayoutOnPaint = p.allowEnableLayoutOnPaint;
}
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
cmp.initComponentImpl();
}
if(leadComponent != null) {
initLead();
}
}
/**
* {@inheritDoc}
*/
public boolean isEnabled() {
// Normally a container shouldn't be a lead component but this happens
// in the GUI builder and this block can cause an infinite recursion
// without the second condition
if(leadComponent != null && leadComponent != this) {
return leadComponent.isEnabled();
}
return super.isEnabled();
}
/**
* removes a Component from the Container, notice that removed component might still have
* a pending repaint in the queue that won't be removed. Calling form.repaint() will workaround
* such an issue.
*
* @param cmp the removed component
*/
public void removeComponent(Component cmp) {
removeComponentImpl(cmp);
}
/**
* Changes the component index of a child component without revalidating or animating. This is useful
* for complex animations or z-order manipulation but might collide with ongoing animations hence the
* package protected nature.
* @param cmp The component to be moved
* @param location The new component index
*/
void setComponentIndex(Component cmp, int location) {
if (location < components.size()) {
components.remove(cmp);
components.add(location, cmp);
}
}
void removeComponentImpl(final Component cmp) {
final AnimationManager a = getAnimationManager();
if(a != null && a.isAnimating()) {
// pretend like the component was already removed
layout.removeLayoutComponent(cmp);
cmp.setParent(null);
final QueuedRemoval removed = new QueuedRemoval(cmp);
changeQueue.add(removed);
a.addAnimation(new ComponentAnimation() {
private boolean alreadyRemoved;
@Override
public boolean isInProgress() {
return false;
}
@Override
protected void updateState() {
if(!alreadyRemoved) {
try {
alreadyRemoved = true;
removeComponentImplNoAnimationSafety(cmp);
} finally {
changeQueue.remove(removed);
}
revalidateLater();
}
}
@Override
public void flush() {
updateAnimationState();
}
});
} else {
removeComponentImplNoAnimationSafety(cmp);
}
}
/**
* removes a Component from the Container
*
* @param cmp the removed component
*/
void removeComponentImplNoAnimationSafety(Component cmp) {
Form parentForm = getComponentForm();
layout.removeLayoutComponent(cmp);
// the deinitizlize contract expects the component to be in a container but if this is a part of an animation
// it might have been removed already to prevent conflict with remove operations
cmp.setParent(this);
cmp.deinitializeImpl();
components.remove(cmp);
cmp.setParent(null);
if (parentForm != null) {
if (parentForm.getFocused() == cmp || cmp instanceof Container && ((Container) cmp).contains(parentForm.getFocused())) {
parentForm.setFocusedInternal(null);
}
Component dragged = parentForm.getDraggedComponent();
if(dragged == cmp){
parentForm.setDraggedComponent(null);
}
if (cmp.isSmoothScrolling()) {
parentForm.deregisterAnimatedInternal(cmp);
}
}
cmp.cancelRepaints();
if(cmp instanceof Form) {
cmp.setVisible(false);
}
setShouldCalcPreferredSize(true);
Display.impl.componentRemoved(cmp);
}
/**
* remove this component and it's children from the painting queue
*/
protected void cancelRepaints() {
super.cancelRepaints();
for (int i = 0; i < getComponentCount(); i++) {
Component c = getComponentAt(i);
c.cancelRepaints();
}
}
/**
* Cleansup the initialization flags in the hierachy
*/
void deinitializeImpl() {
super.deinitializeImpl();
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
cmp.deinitializeImpl();
}
flushReplace();
}
/**
* Flushes ongoing replace operations to prevent two concurrent replace operations from colliding.
* If there is no ongoing replace nothing will occur
* @deprecated this method is no longer used in the new animation framework
*/
public void flushReplace() {
/*if (cmpTransitions != null) {
int size = cmpTransitions.size();
for (int iter = 0; iter < size; iter++) {
((Anim) cmpTransitions.elementAt(iter)).destroy();
}
cmpTransitions.removeAllElements();
cmpTransitions = null;
}*/
}
/**
* remove all Components from container, notice that removed component might still have
* a pending repaint in the queue that won't be removed. Calling form.repaint() will workaround
* such an issue. Notice that this method doesn't recurse and only removes from
* the current container.
*/
public void removeAll() {
Form parentForm = getComponentForm();
if (parentForm != null) {
Component focus = parentForm.getFocused();
if (focus != null && contains(focus)) {
parentForm.setFocused(null);
}
}
// prevents concurrent modification exception
Component[] arr;
boolean includeQueued = true; // Setting this true because when would you ever want removeAll() to NOT remove queued components
if (includeQueued) {
java.util.List l = getChildrenAsList(includeQueued);
arr = new Component[l.size()];
l.toArray(arr);
} else {
arr = new Component[components.size()];
components.toArray(arr);
}
int componentCount = arr.length;
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = arr[iter];
removeComponent(cmp);
}
resetScroll();
}
private boolean revalidatePending;
/**
* Revalidates the container in a way that doesn't conflict with
* running animations. If you simply call {@link #revalidate() }
* on a container while an animation is in progress, it will produce
* paint artifacts as it will insert frames in the animation with
* the container at its final position. Using this method, it will
* wait until running animations are complete before it revalidates.
*
* @since 6.0
*/
public void revalidateWithAnimationSafety() {
if (revalidatePending) {
return;
}
revalidatePending = true;
AnimationManager mgr = getAnimationManager();
if (mgr == null) {
revalidatePending = false;
revalidate();
return;
}
if (mgr.isAnimating()) {
mgr.flushAnimation(new Runnable() {
@Override
public void run() {
revalidatePending = false;
revalidate();
}
});
} else {
revalidatePending = false;
revalidate();
}
}
void revalidateWithAnimationSafetyInternal(final boolean fromRoot) {
if (revalidatePending) {
return;
}
revalidatePending = true;
AnimationManager mgr = getAnimationManager();
if (mgr == null) {
revalidatePending = false;
revalidateInternal(fromRoot);
return;
}
if (mgr.isAnimating()) {
mgr.flushAnimation(new Runnable() {
@Override
public void run() {
revalidatePending = false;
revalidateInternal(fromRoot);
}
});
} else {
revalidatePending = false;
revalidateInternal(fromRoot);
}
}
/**
* Re-layout the container, this is useful when we modify the container hierarchy and
* need to redo the layout
*/
public void revalidate() {
revalidateInternal(true);
}
/**
* Internal revalidate method. Takes parameter {@literal fromRoot} that
* allows you to disable the default behaviour of revalidating the form.
* @param fromRoot
*/
void revalidateInternal(boolean fromRoot) {
setShouldCalcPreferredSize(true);
Form root = getComponentForm();
if (root != null && root != this) {
root.removeFromRevalidateQueue(this);
if (fromRoot && root.revalidateFromRoot) {
root.layoutContainer();
root.repaint();
// for complex hierarchies
if(getParent() != null) {
getParent().shouldLayout = true;
getParent().layoutContainer();
} else {
layoutContainer();
}
} else {
layoutContainer();
repaint();
}
} else {
layoutContainer();
repaint();
}
}
/**
* Revalidates the container before the next paint cycle. Prefer this
* method to {@link #revalidate() } and {@link #revalidateWithAnimationSafety() }
* if you don't need the revalidate (layout and repaint) to happen immediately,
* but you *do* want it to happen before the next paint. This is can be far more
* efficient as it will squash the revalidation calls into the minimal set
* of containers that require revalidation, so that the system doesn't end up
* revalidating the same container multiple times between paints.
*
*/
public void revalidateLater() {
Form root = getComponentForm();
if (root != null) {
root.revalidateLater(this);
}
}
/**
* A more powerful form of revalidate that recursively lays out the full hierarchy
*/
public void forceRevalidate() {
forceRevalidateImpl();
revalidate();
}
private void forceRevalidateImpl() {
setShouldCalcPreferredSize(true);
int c = getComponentCount();
for(int iter = 0 ; iter < c ; iter++) {
Component cmp = getComponentAt(iter);
if(cmp instanceof Container) {
((Container)cmp).forceRevalidateImpl();
} else {
cmp.setShouldCalcPreferredSize(true);
}
}
}
/**
* {@inheritDoc}
*/
public void clearClientProperties(){
super.clearClientProperties();
int c = getComponentCount();
for(int iter = 0 ; iter < c ; iter++) {
Component cmp = getComponentAt(iter);
cmp.clearClientProperties();
}
}
private void paintContainerChildrenForAnimation(Container cnt, Graphics g) {
int ourX = getAbsoluteX();
int ourY = getAbsoluteY();
int cc = cnt.getComponentCount();
for(int iter = 0 ; iter < cc ; iter++) {
Component cmp = cnt.getComponentAt(iter);
if(cmp.getClass() == Container.class) {
paintContainerChildrenForAnimation((Container)cmp, g);
continue;
}
int abx = cmp.getAbsoluteX();
int aby = cmp.getAbsoluteY();
int oldX = cmp.getX();
int oldY = cmp.getY();
cmp.setParent(this);
cmp.setX(abx - ourX);
cmp.setY(aby - ourY);
cmp.paintInternal(g, false);
cmp.setParent(cnt);
cmp.setX(oldX);
cmp.setY(oldY);
}
}
static boolean blockOverdraw = false;
/**
* Invoked internally to indicate if child components are hiding this container
* thus removing the need to invoke its own paint methods
* @return true if child components are obscuring this component
*/
boolean isObscuredByChildren() {
if(!blockOverdraw) {
return false;
}
if(!getLayout().obscuresPotential(this)) {
return false;
}
Style s = getStyle();
if(s.getPaddingTop() != 0 || s.getPaddingLeftNoRTL()!= 0 || s.getPaddingRightNoRTL()!= 0 || s.getPaddingBottom() != 0) {
return false;
}
int size = components.size();
for(int iter = 0 ; iter < size ; iter++) {
Component cmp = components.get(iter);
s = cmp.getStyle();
if(cmp.getWidth() == 0 || cmp.getHeight() == 0) {
continue;
}
// need to think of a better way, this means we invoke the same logic recurisvely again and again by a factor of depth. Not good...
if(cmp instanceof Container) {
if(!((Container)cmp).getLayout().obscuresPotential(this)) {
return false;
}
if(s.getOpacity() != 0xff || s.getMarginTop() != 0 || s.getMarginLeftNoRTL() != 0 || s.getMarginRightNoRTL() != 0 || s.getMarginBottom()!= 0) {
return false;
}
if((s.getBgTransparency() & 0xff) != 0xff && !((Container)cmp).isObscuredByChildren()) {
return false;
}
} else {
if((s.getBgTransparency() & 0xff) != 0xff || s.getOpacity() != 0xff || s.getMarginTop()!= 0 || s.getMarginLeftNoRTL()!= 0 || s.getMarginRightNoRTL()!= 0 || s.getMarginBottom()!= 0) {
return false;
}
}
}
return true;
}
/**
* Efficiently finds the first child component that is visible in the specified
* bounds.
* This is only really helpful if the child components are sorted
* in some way so that we can quickly (with a binary search) find the first
* visible component. E.g. In BoxLayout.Y_AXIS, the components are arranged
* vertically in order of their index so we can use a binary search to find
* the first visible element. For most other layout managers we can't as easily
* do a sort like this.
*
* If the layout manager doesn't allow for a binary search, then this will
* just return 0 (meaning that you need to scan the children from the beginning
* to find visible children).
*
* After you obtain this value, use the {@link #calculateLastPaintableOffset(int, int, int, int, int) } method
* to get the end of the visible region.
*
* The motivation for this is to try to improve performance in places where the container
* has many (say 2500) children, and most of them aren't actually visible.
*
* @param clipX1 Left bounds of region to check. (0,0) is the top left corner of this component.
* @param clipY1 Top bounds of region to check. (0,0) is top left corner of this component.
* @param clipX2 Right bounds of region to check. (0,0) is top left corner of this component.
* @param clipY2 Bottom bounds of region to check. (0,0) is top left corner of this component.
* @return The index within the "components" array where the first child that intersects the provided
* clip occurs, or -1 if there is no "fast" way to find it. If there was a fast way to do it, but no visible
* components were found, then this will return components.size().
*
* @see #calculateLastPaintableOffset(int, int, int, int, int)
*/
private int calculateFirstPaintableOffset(int clipX1, int clipY1, int clipX2, int clipY2) {
int len = components.size();
Layout l = getLayout();
if (l.getClass() == BoxLayout.class) {
if (((BoxLayout)l).getAxis() == BoxLayout.Y_AXIS) {
// Use a binary search to find the first visible
int startPos = binarySearchFirstIntersectionY(clipY1, clipY2, 0, len);
if (startPos >= 0) {
return startPos;
} else {
return len;
}
}
}
return -1;
}
/**
* Gets the index of the "last" child component that intersects the given rectangle. This is
* only helpful if the components are sorted (e.g. with BoxLayout.Y_AXIS). If they aren't
* sorted then this will just return components.size()-1.
* @param pos The starting position to search. It is assumed that this starting
* position is in the visible region.
* @param clipX1 The left bounds of the region to search. (0,0) is the top left corner of the container.
* @param clipY1 The top bounds of the region to search. (0,0) is the top left corner of the container.
* @param clipX2 The right bounds of the region to search. (0,0) is the top left corner of the container.
* @param clipY2 The bottom bounds of the region to search. (0,0) is the top left corner of the container.
* @return The index of the last visible component in this container - or components.size()-1
*/
private int calculateLastPaintableOffset(int pos, int clipX1, int clipY1, int clipX2, int clipY2) {
final int len = components.size();
if (pos >= len-1) {
// Start position is after the last index, so we didn't
// even find an end offset.
// Let's return one less than pos to indicate this
return len-1;
}
final Layout l = getLayout();
if (l.getClass() == BoxLayout.class) {
if (((BoxLayout)l).getAxis() == BoxLayout.Y_AXIS) {
// Use a binary search to find the first visible
//Component c = components.get(++pos);
Component c = null;
int cy1 = -1;
final int end = len-1;
pos++; // This should still be a valid index because
// we previously checked to see if it was >= len-1
do {
c = components.get(pos);
cy1 = c.getBounds().getY();
} while (++pos <= end && cy1 <= clipY2);
return pos-1;
}
}
return len-1;
}
/**
* Performs a binary search within the children of the container to find components
* that intersect the given range on the y-axis. This should only be used
* if it is known that the child components are sorted by their y coordinates
* in ascending order. Otherwise you'll get undefined results.
* @param y1 The lower y-bound of the region to search. (0,0) is top-left corner of container.
* @param y2 The upper y-bound of the region to search. (0,0) is top-left corner of container.
* @param start The lower "index" to search.
* @param end The upper "index" to search.
* @return The index within the components array of the first child component
* that intersects the given region. Or -1 if none is found.
*/
private int binarySearchFirstIntersectionY(int y1, int y2, int start, int end) {
if (start >= end) {
return -1;
}
int pos = (start + end) /2;
Component c = components.get(pos);
Rectangle bounds = c.getBounds();
int cy1 = bounds.getY();
int cy2 = bounds.getY() + bounds.getHeight();
if ((cy1 >= y1 && cy1<= y2)||(cy2>=y1 && cy2 <=y2)||(cy1<=y1 && cy2>=y2)) {
// We have a hit let's roll backward until we find the first visible
while (pos > start && cy1 > y1) {
c = components.get(--pos);
cy1 = c.getBounds().getY();
}
return pos;
} else if (cy1 > y2) {
return binarySearchFirstIntersectionY(y1, y2, start, pos);
} else {
return binarySearchFirstIntersectionY(y1, y2, pos+1, end);
}
}
/**
* Activates enableLayoutOnPaint behaviour for this container. This is package private because
* this flag is more complicated than a simple setter. When the container is initialized
* it will take on the value of its parent, so it only makes sense to call this method on the
* top-level container, like a Form. Form overrides this method and makes it public.
*
* Development Note: enableLayoutOnPaint causes the container to be laid out whenever paint()
* is called. This has been part of codename one since the beginning (initial commit to google code), but
* this taxes rendering performance fairly seriously in some of the complex layouts, and it isn't clear
* why it was ever necessary. Perhaps it was to help in an edge case that is no longer relevant.
*
* We are adding this additionally flag which defaults to false to try to gain performance, and just in
* case the edge case still exists, developers are able to "enable" it again on a form-by-form basis.
* @param allow Whether to allow enable layout on paint.
* @since 7.0
* @see #enableLayoutOnPaint
*/
void setAllowEnableLayoutOnPaint(boolean allow) {
allowEnableLayoutOnPaint = allow;
}
/**
* Set to keep track of elevated components to render against this surface.
*/
private HashSet elevatedComponents;
/**
* Registers a component with this surface as an elevated component.
* @param cmp
*/
void addElevatedComponent(Component cmp) {
if (elevatedComponents == null) elevatedComponents = new HashSet();
elevatedComponents.add(cmp);
}
/**
* Unregisters a component with this surface as an elevated components.
* @param cmp
*/
void removeElevatedComponent(Component cmp) {
if (elevatedComponents == null) return;
elevatedComponents.remove(cmp);
}
/**
* A set used in {@link #paintElevatedPane(Graphics)} to gather all of the elevated descendent components
* of this container.
*/
ArrayList _tmpRenderingElevatedComponents;
/**
* Paints the all of the elevated components in this surface.
* @param g
*/
void paintElevatedPane(Graphics g) {
nextElevationComponentIndex = 0;
paintElevatedPane(g, false, -1, -1, -1, -1, -1, -1, false);
}
/**
* Index variable used to assign indices to components within the same elevation level.
*/
private int nextElevationComponentIndex;
/**
* Paints the elevated pane for a surface.
* @param g THe graphics context
* @param useIntersection Enable intersection checking. This is used when trying to paint components above and below other components,
* as it checks the intersection for painting.
* @param intersectionX IntersectionX in abs screen coords.
* @param intersectionY The intersectonY in abs screen coords
* @param intersectionWidth THe intersection width in abs screen coords
* @param intersectionHeight The intersection height in abs screen coords
* @param elevationThreshold The elevation threshold used when useIntersection is true. If above is true, then this threshold is used to paint
* only the components on the same elevation level and higher.
* @param elevationComponentIndexThreshold The elevation component index threshold used when useIntersection is true. This is used to differentiate the
* z-index of components in the same elevation level.
* @param above Indicate whether to render components above or below the thresholds specified by elevationThreshold and elevationComponentIndexThreshold. Only used if useIntersection is true.
*/
void paintElevatedPane(Graphics g, final boolean useIntersection, int intersectionX, int intersectionY, int intersectionWidth, int intersectionHeight, int elevationThreshold, int elevationComponentIndexThreshold, boolean above) {
CodenameOneImplementation impl = Display.impl;
int absX = getAbsoluteX();
int absY = getAbsoluteY();
g.translate(-absX, -absY);
if (elevatedComponents != null && !elevatedComponents.isEmpty()) {
if (_tmpRenderingElevatedComponents == null) _tmpRenderingElevatedComponents = new ArrayList(elevatedComponents);
else {
_tmpRenderingElevatedComponents.clear();
_tmpRenderingElevatedComponents.addAll(elevatedComponents);
}
Collections.sort(_tmpRenderingElevatedComponents, new Comparator() {
public int compare(Component o1, Component o2) {
int e1 = o1.getStyle().getElevation();
int e2 = o2.getStyle().getElevation();
if (e1 < e2) return -1;
else if (e1 > e2) return 1;
else {
return o1.renderedElevationComponentIndex - o2.renderedElevationComponentIndex;
}
}
});
for (Component child : _tmpRenderingElevatedComponents) {
int relativeX = child.getRelativeX(this) + child.getScrollX();
int relativeY = child.getRelativeY(this) + child.getScrollY();
int clipX = g.getClipX();
int clipW = g.getClipWidth();
int shadowX = relativeX + child.calculateShadowOffsetX();
int shadowW = child.calculateShadowWidth();
if (shadowX + shadowW <= clipX || shadowX >= clipX + clipW) continue;
int clipY = g.getClipY();
int clipH = g.getClipHeight();
int shadowY = relativeY + child.calculateShadowOffsetY();
int shadowH = child.calculateShadowHeight();
if (shadowY + shadowH <= clipY || shadowY >= clipY + clipH) continue;
if (!useIntersection || Rectangle.intersects(child.getAbsoluteX() + child.getScrollX() + child.calculateShadowOffsetX(),
child.getAbsoluteY() + child.getScrollY() + child.calculateShadowOffsetY(),
child.calculateShadowWidth(),
child.calculateShadowHeight(),
intersectionX, intersectionY, intersectionWidth, intersectionHeight)
) {
if (!useIntersection) {
child.renderedElevation = child.getStyle().getElevation();
child.renderedElevationComponentIndex = nextElevationComponentIndex++;
}
if (!useIntersection || elevationThreshold < 0 ||
(above && (elevationThreshold < child.renderedElevation || elevationThreshold == child.renderedElevation && elevationComponentIndexThreshold < child.renderedElevationComponentIndex)) ||
(!above && (elevationThreshold > child.renderedElevation || elevationThreshold == child.renderedElevation && elevationComponentIndexThreshold > child.renderedElevationComponentIndex))) {
g.translate(absX, absY);
child.paintShadows(impl.getComponentScreenGraphics(this, g), child.getRelativeX(this), child.getRelativeY(this));
g.translate(-absX, -absY);
int tx = child.getParent().getRelativeX(this) + child.getScrollX();
int ty = child.getParent().getRelativeY(this) + child.getScrollY();
g.translate(tx, ty);
child.paintInternal(impl.getComponentScreenGraphics(this, g), false);
g.translate(-tx, -ty);
}
}
Container cnt = child.getParent();
Component currCmp = child;
boolean foundOverlap = false;
// We need to paint all components that should be "on top" of the elevated component
// also.
paintOnTopLoop: while (cnt != this && cnt != null) {
Layout cntLayout = cnt.getLayout();
if (!foundOverlap && cntLayout.isOverlapSupported()) foundOverlap = true;
if (foundOverlap) {
int currCmpIndex = cnt.getComponentIndex(currCmp);
if (currCmpIndex >= 0) {
int count = cnt.getComponentCount();
for (int i=currCmpIndex+1; i < count; i++) {
Component cntChild = cnt.getComponentAt(i);
if (elevatedComponents.contains(cntChild)) {
// if this component is itself an elevated component
// then it, and all of its subsequent
break paintOnTopLoop;
}
if (!useIntersection || Rectangle.intersects(cntChild.getAbsoluteX() + cntChild.getScrollX(), cntChild.getAbsoluteY() + cntChild.getScrollY(), cntChild.getWidth(), cntChild.getHeight(),
intersectionX, intersectionY, intersectionWidth, intersectionHeight)
) {
if (!useIntersection) {
cntChild.renderedElevation = child.renderedElevation;
cntChild.renderedElevationComponentIndex = nextElevationComponentIndex++;
}
if (!useIntersection || elevationThreshold < 0 ||
(above && (elevationThreshold < cntChild.renderedElevation || elevationThreshold == cntChild.renderedElevation && elevationComponentIndexThreshold < cntChild.renderedElevationComponentIndex)) ||
(!above && (elevationThreshold > cntChild.renderedElevation || elevationThreshold == cntChild.renderedElevation && elevationComponentIndexThreshold > cntChild.renderedElevationComponentIndex))) {
int tx = cntChild.getParent().getRelativeX(this) + cntChild.getParent().getScrollX();
int ty = cntChild.getParent().getRelativeY(this) + cntChild.getParent().getScrollY();
g.translate(tx, ty);
cntChild.paintInternal(impl.getComponentScreenGraphics(this, g), false);
g.translate(-tx, -ty);
}
}
}
}
}
currCmp = cnt;
cnt = cnt.getParent();
}
}
}
g.translate(absX, absY);
}
/**
* This is used to "tag" components in this surface that should be rendered in the elevated pane.
* This just sets or unsets the {@link Component#doNotPaint} flag so that rendering of the non-elevated
* pane can proceed without rendering elevated components.
*
*
* @param shouldPaintInElevatedPane True if we are setting the doNotPaint flag. False if we are unsetting it.
*/
void markComponentsToBePaintedInElevatedPane(boolean shouldPaintInElevatedPane) {
if (elevatedComponents != null && !elevatedComponents.isEmpty()) {
for (Component child : elevatedComponents) {
child.doNotPaint = shouldPaintInElevatedPane;
Container cnt = child.getParent();
Component currCmp = child;
boolean foundOverlap = false;
// We need to paint all components that should be "on top" of the elevated component
// also.
paintOnTopLoop: while (cnt != this && cnt != null) {
Layout cntLayout = cnt.getLayout();
if (!foundOverlap && cntLayout.isOverlapSupported()) foundOverlap = true;
if (foundOverlap) {
int currCmpIndex = cnt.getComponentIndex(currCmp);
if (currCmpIndex >= 0) {
int count = cnt.getComponentCount();
for (int i=currCmpIndex+1; i < count; i++) {
Component cntChild = cnt.getComponentAt(i);
if (elevatedComponents.contains(cntChild)) {
// if this component is itself an elevated component
// then it, and all of its subsequent
break paintOnTopLoop;
}
child.doNotPaint = shouldPaintInElevatedPane;
}
}
}
currCmp = cnt;
cnt = cnt.getParent();
}
}
}
}
/**
* {@inheritDoc}
*/
public void paint(Graphics g) {
// Q: Why two flags for enableLayoutOnPaint?
// A: enableLayoutOnPaint is managed internally, and it enabled/renabled
// in CN1 code during certain performance-sensitive periods.
// allowEnableLayoutOnPaint is a flag controlled by the developer so that
// they can enable/disable this behaviour at form level via the setAllowEnableLayoutOnPaint(boolean)
// method. See javadocs for Form.setAllowEnableOnPaint(boolean) for historical background
// this feature.
if (allowEnableLayoutOnPaint && enableLayoutOnPaint) {
layoutContainer();
}
g.translate(getX(), getY());
int size = components.size();
int startIter = 0;
if (size >= 30) {
int clipX1 = g.getClipX();
int clipX2 = g.getClipX() + g.getClipWidth();
int clipY1 = g.getClipY();
int clipY2 = g.getClipY() + g.getClipHeight();
startIter = calculateFirstPaintableOffset(clipX1, clipY1, clipX2, clipY2);
if (startIter < 0) {
// There was no efficient way to calculate the offset
startIter = 0;
} else if (startIter < size) {
// There was an efficient way to calculate the offset so we
// will continue this approach
size = calculateLastPaintableOffset(startIter, clipX1, clipY1, clipX2, clipY2) + 1;
}
}
if (isSurface() && elevatedComponents != null && !elevatedComponents.isEmpty()) {
// We need to mark all of the elevated components so that they don't render the first time around
markComponentsToBePaintedInElevatedPane(true);
}
CodenameOneImplementation impl = Display.impl;
if (dontRecurseContainer) {
for (int iter = startIter; iter < size; iter++) {
Component cmp = components.get(iter);
if (cmp.getClass() == Container.class) {
paintContainerChildrenForAnimation((Container) cmp, g);
} else {
cmp.paintInternal(impl.getComponentScreenGraphics(this, g), false);
}
}
} else {
for (int iter = startIter; iter < size; iter++) {
Component cmp = components.get(iter);
cmp.paintInternal(impl.getComponentScreenGraphics(this, g), false);
}
}
if (isSurface() && elevatedComponents != null && !elevatedComponents.isEmpty()) {
markComponentsToBePaintedInElevatedPane(false);
paintElevatedPane(g);
}
int tx = g.getTranslateX();
int ty = g.getTranslateY();
g.translate(-tx, -ty);
if(sidemenuBarTranslation > 0) {
g.translate(sidemenuBarTranslation, 0);
paintGlass(g);
paintTensile(g);
g.translate(-sidemenuBarTranslation, 0);
} else {
paintGlass(g);
paintTensile(g);
}
g.translate(tx, ty);
g.translate(-getX(), -getY());
}
/**
* This method can be overriden by a component to draw on top of itself or its children
* after the component or the children finished drawing in a similar way to the glass
* pane but more refined per component
*
* @param g the graphics context
*/
protected void paintGlass(Graphics g) {
}
void paintGlassImpl(Graphics g) {
super.paintGlassImpl(g);
paintGlass(g);
}
void paintIntersecting(Graphics g, Component cmp, int x, int y, int w, int h, boolean above, int elevation) {
if (layout.isOverlapSupported() && cmp.getParent() == this) {
int indexOfComponent = components.indexOf(cmp);
int startIndex;
int endIndex;
if (above) {
startIndex = indexOfComponent + 1;
endIndex = components.size();
} else {
startIndex = 0;
endIndex = indexOfComponent;
}
for (int i = startIndex; i < endIndex; i++) {
Component cmp2 = (Component) components.get(i);
if (cmp2.renderedElevation != elevation) continue;
if(Rectangle.intersects(x, y, w, h,
cmp2.getAbsoluteX() + cmp2.getScrollX(),
cmp2.getAbsoluteY() + cmp2.getScrollY(),
cmp2.getBounds().getSize().getWidth(),
cmp2.getBounds().getSize().getHeight())){
cmp2.paintInternal(g, false);
}
}
}
}
/**
* Performs the layout of the container if a layout is necessary
*/
public void layoutContainer() {
//will compute the container + components and will layout the components.
if (shouldLayout) {
shouldLayout = false;
doLayout();
}
}
private boolean hasScrollableYParentInternal() {
if (getParent() == null) {
return false;
}
if (getParent().scrollableYFlag()) {
return true;
}
return getParent().hasScrollableYParentInternal();
}
private boolean hasScrollableXParentInternal() {
if (getParent() == null) {
return false;
}
if (getParent().scrollableXFlag()) {
return true;
}
return getParent().hasScrollableXParentInternal();
}
/**
* Flag to
*/
private boolean safeArea;
/**
* Indicates that this container is a "safe area" root.
*/
private boolean safeAreaRoot;
/**
* Marks this container as a "safe area", meaning that it will automatically supply
* sufficient padding as necessary for its children to be laid out inside the
* safe area of the screen.
*
* This was primarily added for the iPhone X which covers portions of the screen
* and may interfere with components that are rendered there.
*
* The "safe" area is calculated against a "safe area root"'s bounds, which is
* the parent form by default. In some cases it may be helpful to make the root
* a sub-container, such as if you need to lay a component out off-screen. See
* {@link #setSafeAreaRoot(boolean)} for more details.
*
* @param safeArea True to make this container a safe area.
* @since 7.0
* @see Form#getSafeArea()
* @see #isSafeArea()
* @see #setSafeAreaRoot(boolean)
*/
public void setSafeArea(boolean safeArea) {
this.safeArea = safeArea;
}
/**
* Checks if this container is a "safe area". A "safe area" is a container whose
* contents will always be displayed inside the device's "safe display area".
* This feature was added primarily for the iPhone X which covers some parts of
* the screen and would cover or interfere with any content drawn in those regions. In particular,
* the notch, the rounded corners, and the task bar cover portions of the screen.
*
* A container that is a safe area will automatically add appropriate padding
* on layout so that its children will be rendered completely in the safe area of
* the screen. This only applies if the container has no scrollable parents. If a
* "safe" container has scrollable parents, then it is assumed that the user can
* just scroll it into a safe area.
*
* @return True if this container is a safe area.
* @since 7.0
* @see #setSafeArea(boolean)
* @see Form#getSafeArea()
*/
public boolean isSafeArea() {
return this.safeArea;
}
/**
* Set whether this container is a safe area root. A safe area root is a container
* against whose bounds, safe area margins are calculated for child components.
*
* Safe Area root vs Safe Area
*
* A Safe Area root is not actually a safe area. It will lay out its children
* normally, without any adjustments to padding to accommodate the display safe area. They
* are rather used by safe area child containers to calculate safe area margins,
* according to if the safe area root container spanned the entire screen
*
* In most cases you don't need to explicitly set a safe area root, since Forms are
* marked as roots by default. However, there are edge cases where components may be
* initially laid out off-screen (in which safe areas are not applied), but are transitioned
* in. Once on the screen, the safe margins would be applied which may cause an abrupt
* re-layout at the moment that the safe margins are applied. This edge case occurs in,
* for example, a side menu bar which is rendered off-screen. By making the side menu bar
* container a "root" itself, the safe areas will be applied to the layout, even when
* the menu is off-screen. Then there is no "jerk" when it transitions in.
*
* @param root True to make this a root. False to make it "not" a root.
*
* @since 7.0
* @see #isSafeAreaRoot()
*/
public void setSafeAreaRoot(boolean root) {
this.safeAreaRoot = root;
}
/**
* Checks if this container is a safe area root. A safe area root is a container
* against whose bounds, safe area margins are calculated for child components.
*
* Forms are safe area roots by default.
* @return
* @since 7.0
* @see #setSafeAreaRoot(boolean)
*/
public boolean isSafeAreaRoot() {
return safeAreaRoot;
}
/**
* Gets the Safe area "root" container for this container. This method will walk
* up the component hierarchy until is finds a Container with {@link #isSafeAreaRoot() } true.
*
* Forms are safe area roots by default, but it is possible to mark other containers
* as safe area roots.
*
* A safe area root is a container from which safe area margins are applied when
* calculating the safe areas of child components. Setting a root can facilitate the
* layout of a container's children before it appears on the screen.
* @return
* @since 7.0
*/
public Container getSafeAreaRoot() {
if (safeAreaRoot) {
return this;
}
Container parent = getParent();
if (parent != null) {
return parent.getSafeAreaRoot();
}
return null;
}
/**
* Checks to see if this container or any of its parents are safe areas.
* @param checkParents True to check parents too. False to just check this container.
* @return
*/
private boolean isSafeAreaInternal(boolean checkParents) {
if (safeArea) {
return true;
}
if (checkParents) {
Container parent = getParent();
if (parent != null) {
return parent.isSafeAreaInternal(true);
}
}
return false;
}
/**
* For iPhone X primarily. This will check if the current bounds goes outside the
* safe area. If so, it will add padding to make the contents fit the safe area.
*/
private boolean snapToSafeAreaInternal() {
if (isHidden()) {
return false;
}
Container safeAreaRoot = getSafeAreaRoot();
if (safeAreaRoot == null) {
return false;
}
Rectangle rect = Display.impl.getDisplaySafeArea(new Rectangle());
int safeLeftMargin = rect.getX();
int safeRightMargin = CN.getDisplayWidth() - rect.getWidth() - rect.getX();
int safeTopMargin = rect.getY();
int safeBottomMargin = CN.getDisplayHeight() - rect.getHeight() - rect.getY();
if (safeLeftMargin == 0 && safeRightMargin == 0 && safeBottomMargin == 0 && safeTopMargin == 0) {
return false;
}
rect.setWidth(Math.max(0, safeAreaRoot.getWidth() - safeLeftMargin - safeRightMargin));
rect.setHeight(Math.max(0, safeAreaRoot.getHeight() - safeTopMargin - safeBottomMargin));
if (rect.getWidth() == 0 || rect.getHeight() == 0) {
return false;
}
Rectangle safeArea = rect;
//Form f = getComponentForm();
//if (f == null) {
// return false;
//}
//Rectangle safeArea = f.getSafeArea();
//if (safeArea.getX() == 0 && safeArea.getY() == 0 && safeArea.getWidth() == CN.getDisplayWidth() && safeArea.getHeight() == CN.getDisplayHeight()) {
// return false;
//}
Style style = getStyle();
int safeX1 = safeArea.getX();
int safeX2 = safeArea.getWidth() + safeX1;
int safeY1 = safeArea.getY();
int safeY2 = safeArea.getHeight() + safeY1;
int paddingLeft = style.getPaddingLeftNoRTL();
int paddingRight = style.getPaddingRightNoRTL();
int paddingTop = style.getPaddingTop();
int paddingBottom = style.getPaddingBottom();
int newPaddingTop = paddingTop;
int newPaddingBottom = paddingBottom;
int newPaddingLeft = paddingLeft;
int newPaddingRight = paddingRight;
int absX = getAbsoluteX() - safeAreaRoot.getAbsoluteX();
int w = getWidth();
int absX2 = absX + w;
if (absX >= 0) {
if (absX + paddingLeft < safeX1) {
newPaddingLeft = safeX1 - absX;
}
}
if (absX2 <= safeAreaRoot.getWidth()) {
if (absX2 - paddingRight > safeX2) {
newPaddingRight = absX2 - safeX2;
}
}
int absY = getAbsoluteY() - safeAreaRoot.getAbsoluteY();
int h = getHeight();
int absY2 = absY + h;
if (absY >= 0) {
if (absY + paddingTop < safeY1) {
newPaddingTop = safeY1 - absY;
}
}
if (absY2 <= safeAreaRoot.getHeight()) {
if (absY2 - paddingBottom > safeY2) {
newPaddingBottom = absY2 - safeY2;
}
}
boolean changed = false;
if (newPaddingTop != paddingTop || newPaddingBottom != paddingBottom) {
if (!hasScrollableYParentInternal()) {
changed = true;
if (newPaddingTop != paddingTop) {
style.setPaddingUnitTop(Style.UNIT_TYPE_PIXELS);
style.setPaddingTop(newPaddingTop);
}
if (newPaddingBottom != paddingBottom) {
style.setPaddingUnitBottom(Style.UNIT_TYPE_PIXELS);
style.setPaddingBottom(newPaddingBottom);
}
}
}
if (newPaddingLeft != paddingLeft || newPaddingRight != paddingRight) {
if (!hasScrollableXParentInternal()) {
changed = true;
if (newPaddingLeft != paddingLeft) {
style.setPaddingUnitLeft(Style.UNIT_TYPE_PIXELS);
style.setPaddingLeft(newPaddingLeft);
}
if (newPaddingRight != paddingRight) {
style.setPaddingUnitRight(Style.UNIT_TYPE_PIXELS);
style.setPaddingRight(newPaddingRight);
}
}
}
return changed;
}
/**
* Lays out the container
*/
private static class TmpInsets {
float top, left, bottom, right;
byte topUnit, leftUnit, bottomUnit, rightUnit;
@Override
public String toString() {
return top+","+right+","+bottom+","+left;
}
private void set(Style style){
//boolean suppressEvents = style.isSuppressChangeEvents();
//style.setSuppressChangeEvents(true);
top = style.getPaddingFloatValue(false, TOP);
left = style.getPaddingFloatValue(false, LEFT);
bottom = style.getPaddingFloatValue(false, BOTTOM);
right = style.getPaddingFloatValue(false, RIGHT);
byte[] units = style.getPaddingUnit();
if (units != null) {
topUnit = units[TOP];
leftUnit = units[LEFT];
bottomUnit = units[BOTTOM];
rightUnit = units[RIGHT];
} else {
topUnit = leftUnit = bottomUnit = rightUnit = Style.UNIT_TYPE_PIXELS;
}
//style.setSuppressChangeEvents(suppressEvents);
}
private void restore(Style style) {
boolean suppressEvents = style.isSuppressChangeEvents();
style.setSuppressChangeEvents(true);
style.setPadding(TOP, top, true);
style.setPadding(LEFT, left, true);
style.setPadding(BOTTOM, bottom, true);
style.setPadding(RIGHT, right, true);
byte[] units = style.getPaddingUnit();
if (units != null) {
units[TOP] = topUnit;
units[BOTTOM] = bottomUnit;
units[LEFT] = leftUnit;
units[RIGHT] = rightUnit;
} else {
style.setPaddingUnit(topUnit, leftUnit, bottomUnit, rightUnit);
}
style.setSuppressChangeEvents(suppressEvents);
}
}
private TmpInsets tmpInsets;
private int doLayoutDepth;
void doLayout() {
doLayoutDepth++;
boolean restoreBounds = false;
if (safeArea && doLayoutDepth == 1) {
// If this container is marked as a safe area
// then we may need to add padding to make it *safe*
Container parent = getParent();
if (parent == null || !parent.isSafeAreaInternal(true)) {
// For efficiency, we check if the parent is a safe area.
// If so, we don't need to worry because it has already
// added appropriate padding.
if (tmpInsets == null) {
tmpInsets = new TmpInsets();
}
Style s = getStyle();
tmpInsets.set(s);
restoreBounds = snapToSafeAreaInternal();
}
}
layout.layoutContainer(this);
int count = getComponentCount();
for (int i = 0; i < count; i++) {
Component c = getComponentAt(i);
if (c instanceof Container) {
((Container) c).layoutContainer();
}else{
c.laidOut();
}
}
if (restoreBounds && tmpInsets != null) {
tmpInsets.restore(getStyle());
}
laidOut();
if(Form.activePeerCount > 0) {
onParentPositionChange();
}
doLayoutDepth--;
}
/**
* Returns the number of components
*
* @return the Component count
*/
public int getComponentCount() {
return components.size();
}
/**
* Returns the Component at a given index
*
* @param index of the Component you wish to get
* @return a Component
* @throws ArrayIndexOutOfBoundsException if an invalid index was given.
*/
public Component getComponentAt(
int index) {
return components.get(index);
}
/**
* Returns the Component index in the Container
*
* @param cmp the component to search for
* @return the Component index in the Container or -1 if not found
*/
public int getComponentIndex(Component cmp) {
int count = getComponentCount();
for (int i = 0; i <
count; i++) {
Component c = getComponentAt(i);
if (c.equals(cmp)) {
return i;
}
}
return -1;
}
/**
* Returns true if the given component is within the hierarchy of this container
*
* @param cmp a Component to check
* @return true if this Component contains in this Container
*/
public boolean contains(Component cmp) {
if (cmp == null) {
return false;
}
cmp = cmp.getParent();
while (cmp != null) {
if (cmp == this) {
return true;
}
cmp = cmp.getParent();
}
return false;
}
/**
* Makes sure the component is visible in the scroll if this container is
* scrollable
*
* @param c the component that will be scrolling for visibility
*/
public void scrollComponentToVisible(final Component c) {
if (isScrollable()) {
if (c != null) {
Rectangle r = c.getVisibleBounds();
if (c.getParent() != null) {
// special case for the first component to allow the user to scroll all the
// way to the top
Form f = getComponentForm();
if (f != null && f.getInvisibleAreaUnderVKB() == 0 &&
f.findFirstFocusable() == c) {
// support this use case only if the component doesn't explicitly declare visible bounds
if (r == c.getBounds() && !Display.getInstance().isTouchScreenDevice()) {
scrollRectToVisible(new Rectangle(0, 0,
c.getX() + Math.min(c.getWidth(), getWidth()),
c.getY() + Math.min(c.getHeight(), getHeight())), this);
return;
}
}
}
boolean moveToVisible = true;
Dimension size = r.getSize();
boolean large = size.getHeight() > getHeight() ||
size.getWidth() > getWidth();
if (large) {
int x = getScrollX();
int y = getScrollY();
int w = getWidth();
int h = getHeight();
boolean visible = contains(c) && Rectangle.intersects(c.getAbsoluteX(),
c.getAbsoluteY(),
c.getWidth(),
c.getHeight(),
getAbsoluteX() + x,
getAbsoluteY() + y,
w,
h);
//if this is a big component no need to scroll to the begining if it's
//partially visible
moveToVisible = !visible;
}
if (moveToVisible) {
scrollRectToVisible(r.getX(), r.getY(),
Math.min(r.getSize().getWidth(), getWidth()),
Math.min(r.getSize().getHeight(), getHeight()), c);
}
}
}
}
/**
* This method scrolls the Container if Scrollable towards the given
* Component based on the given direction.
*
* @param direction is the direction of the navigation (Display.GAME_UP,
* Display.GAME_DOWN, ...)
* @param next the Component to move the scroll towards.
*
* @return true if next Component is now visible.
*/
boolean moveScrollTowards(int direction, Component next) {
if (isScrollable()) {
Component current = null;
Form f = getComponentForm();
current = f.getFocused();
boolean cyclic = f.isCyclicFocus();
f.setCyclicFocus(false);
boolean edge = false;
boolean currentLarge = false;
boolean scrollOutOfBounds = false;
int x = getScrollX();
int y = getScrollY();
int w = getWidth();
int h = getHeight();
switch (direction) {
case Display.GAME_UP:
if(!cyclic && getScrollY() == 0){
return true;
}
y = getScrollY() - scrollIncrement;
edge = f.findNextFocusUp() == null;
currentLarge = (current != null && current.getVisibleBounds().getSize().getHeight() > getHeight());
scrollOutOfBounds = y < 0;
if(scrollOutOfBounds){
y = 0;
}
break;
case Display.GAME_DOWN:
y = getScrollY() + scrollIncrement;
edge = f.findNextFocusDown() == null;
currentLarge = (current != null && current.getVisibleBounds().getSize().getHeight() > getHeight());
scrollOutOfBounds = y > getScrollDimension().getHeight() - getHeight();
if(scrollOutOfBounds){
y = getScrollDimension().getHeight() - getHeight();
}
break;
case Display.GAME_RIGHT:
x = getScrollX() + scrollIncrement;
edge = f.findNextFocusRight() == null;
currentLarge = (current != null && current.getVisibleBounds().getSize().getWidth() > getWidth());
scrollOutOfBounds = x > getScrollDimension().getWidth() - getWidth();
if(scrollOutOfBounds){
x = getScrollDimension().getWidth() - getWidth();
}
break;
case Display.GAME_LEFT:
x = getScrollX() - scrollIncrement;
edge = f.findNextFocusLeft() == null;
currentLarge = (current != null && current.getVisibleBounds().getSize().getWidth() > getWidth());
scrollOutOfBounds = x < 0;
if(scrollOutOfBounds){
x = 0;
}
break;
}
f.setCyclicFocus(cyclic);
//if the Form doesn't contain a focusable Component simply move the
//viewport by pixels
if(next == null || next == this){
scrollRectToVisible(x, y, w, h, this);
return false;
}
//if we are on the edge and this is a non cyclic Form.
if(!cyclic && direction == Display.GAME_DOWN && edge){
scrollRectToVisible(x, y, w, h, this);
return false;
}
boolean nextIntersects = contains(next) && Rectangle.intersects(next.getAbsoluteX(),
next.getAbsoluteY(),
next.getWidth(),
next.getHeight(),
getAbsoluteX() + x,
getAbsoluteY() + y,
w,
h);
if ((nextIntersects && !currentLarge && !edge) || (Rectangle.contains(
getAbsoluteX() + getScrollX(),
getAbsoluteY() + getScrollY(),
w,
h,
next.getAbsoluteX(),
next.getAbsoluteY(),
next.getWidth(),
next.getHeight()))) {
//scrollComponentToVisible(next);
return true;
} else {
if (!scrollOutOfBounds) {
scrollRectToVisible(x, y, w, h, this);
//if after moving the scroll the current focus is out of the
//view port and the next focus is in the view port move
//the focus
if (nextIntersects && !Rectangle.intersects(current.getAbsoluteX(),
current.getAbsoluteY(),
current.getWidth(),
current.getHeight(),
getAbsoluteX() + x,
getAbsoluteY() + y,
w,
h)) {
return true;
}
return false;
} else {
//scrollComponentToVisible(next);
return true;
}
}
}
return true;
}
private int distanceToComponent(Component c, int x, int y) {
int cx = c.getX();
if(x > cx) {
cx += c.getWidth();
if(cx > x) {
cx = x;
}
}
int cy = c.getY();
if(y > cy) {
cy += c.getHeight();
if(cy > y) {
cy = y;
}
}
x = Math.abs(cx - x);
y = Math.abs(cy - y);
return (int)Math.sqrt(x*x+y*y);
}
/**
* Very useful for touch events or drop events that need approximation more than accuracy
* @param x location in container relative coordinates
* @param y location in container relative coordinates
* @return the closest component in the container or null if no component is in the container
*/
public Component getClosestComponentTo(int x, int y) {
int count = getComponentCount();
if(count == 0) {
return null;
}
Component closest = getComponentAt(0);
if(closest.contains(x, y)) {
return closest;
}
int distance = distanceToComponent(closest, x, y);
for(int iter = 1 ; iter < count ; iter++) {
Component current = getComponentAt(iter);
if(current.contains(x, y)) {
return current;
}
int cd = distanceToComponent(current, x, y);
if(cd < distance) {
closest = current;
distance = cd;
}
}
return closest;
}
/**
* Returns the top-most component that responds to pointer events at absolute
* coordinate {@literal (x, y)}. This may return {@literal null} if there are
* no components at this coordinate that respond to pointer events.
*
* Note: This method is stricter than {@link #getComponentAt(int, int) }
* about which component is returned. Whereas {@link #getComponentAt(int, int) } will return
* {@literal this } when there are no matches, as long as it contains {@literal (x, y)}, {@link #getResponderAt(int, int) }
* will return null in this case. {@link #getComponentAt(int, int) } may also return components
* that are not visible or are not enabled. In generaly, if you are trying to retrieve a component
* that responds to pointer events, you should use this method over {@link #getComponentAt(int, int) } unless
* you have a good reason and really know what you are doing.
*
*
* @param x Absolute x-coordinate.
* @param y Absolute y-coordinate.
* @return Top-most component that responds to pointer events at given coordinate. May be {@literal null}.
* @see Component#respondsToPointerEvents()
*/
public Component getResponderAt(int x, int y) {
if (!isVisible() || !contains(x, y)) {
return null;
}
int startIter = 0;
int count = getComponentCount();
if (count > 30) {
int relx = x - getAbsoluteX();
int rely = y - getAbsoluteY();
startIter = calculateFirstPaintableOffset(relx, rely, relx, rely);
if (startIter < 0) {
// There was no efficient way to calculate the first paintable offset
// start counting from 0
startIter = 0;
} else if (startIter < count) {
// We found a start offset using an efficient method
// Find an appropriate end offset.
count = calculateLastPaintableOffset(startIter, relx, rely, relx, rely) + 1;
}
}
for (int i=count-1; i>=startIter; i--) {
Component cmp = getComponentAt(i);
if (cmp.contains(x, y)) {
if (!cmp.isBlockLead() && cmp instanceof Container) {
cmp = ((Container)cmp).getResponderAt(x, y);
}
if (cmp != null && cmp.respondsToPointerEvents()) {
return cmp;
}
}
}
if (respondsToPointerEvents()) {
return this;
}
return null;
}
/**
* Returns a Component at coordinate {@literal (x, y)}.
*
* WARNING: This method may return components that are disabled,
* or invisible, or that do not respond to pointer events. If you are looking for the
* top-most component that responds to pointer events, you should use {@link #getResponderAt(int, int) }
* as it is guaranteed to return a component with {@link Component#respondsToPointerEvents() } {@literal true};
* or {@literal null} if none is found at the coordinate.
*
* @param x absolute screen location
* @param y absolute screen location
* @return a Component if found, null otherwise
* @see Component#contains
* @see #getResponderAt(int, int)
*/
public Component getComponentAt(int x, int y) {
if (!contains(x, y) || !isVisible()) {
return this;
}
int startIter = 0;
int count = getComponentCount();
if (count > 30) {
int relx = x - getAbsoluteX();
int rely = y - getAbsoluteY();
startIter = calculateFirstPaintableOffset(relx, rely, relx, rely);
if (startIter < 0) {
// There was no efficient way to calculate the first paintable offset
// start counting from 0
startIter = 0;
} else if (startIter < count) {
// We found a start offset using an efficient method
// Find an appropriate end offset.
count = calculateLastPaintableOffset(startIter, relx, rely, relx, rely) + 1;
}
}
boolean overlaps = getActualLayout().isOverlapSupported();
Component component = null;
Component top = null;
for (int i = count - 1; i >= startIter; i--) {
Component cmp = getComponentAt(i);
if (cmp.contains(x, y) && cmp.isVisible()) {
component = cmp;
boolean isPotentialCandidate = cmp.respondsToPointerEvents();
if (cmp instanceof Container) {
Component c = ((Container) cmp).getComponentAt(x, y);
if(c != null){
if (top == null) {
if (c.respondsToPointerEvents() || !(c instanceof Container)) {
top = c;
}
}
if (c != cmp) {
Component tmp = c;
if (cmp.isFocusable()) {
isPotentialCandidate = true;
boolean found = false;
while (tmp != cmp && tmp != null) {
if (tmp.isFocusable()) {
// We found a focusable child
// so we will use that.
c = tmp;
found = true;
break;
}
tmp = tmp.getParent();
}
if (!found) {
// Since the container is focusable
// and none of its children are focusable
// we will prefer to take the container over
// its children here.
c = cmp;
}
} else if (cmp.respondsToPointerEvents()){
isPotentialCandidate = true;
while (tmp != cmp && tmp != null) {
if (tmp.respondsToPointerEvents()) {
// We found a child that also responds to
// pointer events so we will use that.
c = tmp;
break;
}
tmp = tmp.getParent();
}
} else {
// In this last case, the parent doesn't respond to pointer events
// so all we want to know is if any of the children respond to pointer events
// so we know if it will be eligible to be returned in the case of an overlapping
// layout.
while (tmp != cmp && tmp != null) {
if (tmp.respondsToPointerEvents()) {
isPotentialCandidate = true;
break;
}
tmp = tmp.getParent();
}
}
component = c;
}
} else {
// No children found here
if (top == null) {
if (cmp.respondsToPointerEvents() || !(cmp instanceof Container)) {
top = cmp;
}
}
}
} else {
if (top == null) {
if (cmp.respondsToPointerEvents() || !(cmp instanceof Container)) {
top = cmp;
}
}
}
if (!overlaps) {
return component;
} else {
if (isPotentialCandidate) {
return component;
}
}
}
}
if (component == null || (!component.respondsToPointerEvents() && top != null)) {
if (top != null) {
return top;
}
}
if (component != null){
return component;
}
return this;
}
/**
* Recursively searches the container hierarchy for a drop target
*
* @param x position in which we are searching for a drop target
* @param y position in which we are searching for a drop target
* @return a drop target or null if no drop target could be found at the x/y position
*/
public Component findDropTargetAt(int x, int y) {
int count = getComponentCount();
for (int i = count - 1; i >= 0; i--) {
Component cmp = getComponentAt(i);
if (cmp.contains(x, y)) {
if (cmp.isDropTarget()) {
return cmp;
}
if (cmp instanceof Container) {
Component component = ((Container) cmp).findDropTargetAt(x, y);
if(component != null) {
return component;
}
}
}
}
return null;
}
/**
* {@inheritDoc}
*/
public void pointerPressed(int x, int y) {
Component leadParent = LeadUtil.leadParentImpl(this);
leadParent.clearDrag();
leadParent.setDragActivated(false);
Component cmp = getComponentAt(x, y);
if (cmp == this) {
super.pointerPressed(x, y);
return;
}
if (cmp != null) {
//give priority to focusable components contained in the Container
if(cmp.isFocusable() || cmp.isGrabsPointerEvents()){
cmp.pointerPressed(x, y);
return;
}
if(isFocusable() || isGrabsPointerEvents()){
super.pointerPressed(x, y);
return;
}
cmp.pointerPressed(x, y);
}
}
private TmpInsets calcTmpInsets;
private int calcPreferredSizeDepth;
/**
* {@inheritDoc}
*/
protected Dimension calcPreferredSize() {
calcPreferredSizeDepth++;
boolean restoreBounds = false;
if (safeArea && getWidth() > 0 && getHeight() > 0 && calcPreferredSizeDepth == 1) {
// If this container is marked as a safe area
// then we may need to add padding to make it *safe*
Container parent = getParent();
if (parent == null || !parent.isSafeAreaInternal(true)) {
// For efficiency, we check if the parent is a safe area.
// If so, we don't need to worry because it has already
// added appropriate padding.
if (calcTmpInsets == null) {
calcTmpInsets = new TmpInsets();
}
Style s = getStyle();
calcTmpInsets.set(s);
restoreBounds = snapToSafeAreaInternal();
}
}
Dimension d = layout.getPreferredSize(this);
Style style = getStyle();
if(style.getBorder() != null && d.getWidth() != 0 && d.getHeight() != 0) {
d.setWidth(Math.max(style.getBorder().getMinimumWidth(), d.getWidth()));
d.setHeight(Math.max(style.getBorder().getMinimumHeight(), d.getHeight()));
}
if(UIManager.getInstance().getLookAndFeel().isBackgroundImageDetermineSize() && style.getBgImage() != null) {
d.setWidth(Math.max(style.getBgImage().getWidth(), d.getWidth()));
d.setHeight(Math.max(style.getBgImage().getHeight(), d.getHeight()));
}
if (restoreBounds && calcTmpInsets != null) {
calcTmpInsets.restore(getStyle());
}
calcPreferredSizeDepth--;
return d;
}
/**
* {@inheritDoc}
*/
protected String paramString() {
String className = layout.getClass().getName();
String layoutStr = className.substring(className.lastIndexOf('.') + 1);
return super.paramString() + ", layout = " + layoutStr +
", scrollableX = " + scrollableX +
", scrollableY = " + scrollableY +
", components = " + getComponentsNames();
}
/**
* Return the container components objects as list of Strings
* @return the container components objects as list of Strings
*/
private String getComponentsNames() {
String ret = "[";
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
String className = cmp.getClass().getName();
ret += className.substring(className.lastIndexOf('.') + 1) + ", ";
}
if (ret.length() > 1) {
ret = ret.substring(0, ret.length() - 2);
}
ret = ret + "]";
return ret;
}
/**
* {@inheritDoc}
*/
public void refreshTheme(boolean merge) {
super.refreshTheme(merge);
int componentCount = components.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component cmp = components.get(iter);
cmp.refreshTheme(merge);
}
}
boolean scrollableXFlag() {
return scrollableX;
}
boolean scrollableYFlag() {
return scrollableY;
}
/**
* {@inheritDoc}
*/
public boolean isScrollableX() {
return scrollableX && (getScrollDimension().getWidth() + getStyle().getHorizontalPadding() > getWidth());
}
/**
* {@inheritDoc}
*/
public boolean isScrollableY() {
Form f = getComponentForm();
int v = 0;
if(f != null) {
v= f.getInvisibleAreaUnderVKB();
}
return scrollableY && (getScrollDimension().getHeight() + getStyle().getVerticalPadding() > getHeight() - v || isAlwaysTensile());
}
/**
* {@inheritDoc}
*/
public int getSideGap() {
// isScrollableY() in the base method is very expensive since it triggers getScrollDimension before the layout is complete!
if(scrollSize == null) {
if (scrollableY && isScrollVisible()) {
return getUIManager().getLookAndFeel().getVerticalScrollWidth();
}
} else {
return super.getSideGap();
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getBottomGap() {
// isScrollableY() in the base method is very expensive since it triggers getScrollDimension before the layout is complete!
if (scrollableX && isScrollVisible()) {
return getUIManager().getLookAndFeel().getHorizontalScrollHeight();
}
return 0;
}
/**
* Sets whether the component should/could scroll on the X axis
*
* @param scrollableX whether the component should/could scroll on the X axis
*/
public void setScrollableX(boolean scrollableX) {
if(layout instanceof BorderLayout) {
this.scrollableX = false;
} else {
this.scrollableX = scrollableX;
}
}
/**
* Sets whether the component should/could scroll on the Y axis
*
* @param scrollableY whether the component should/could scroll on the Y axis
*/
public void setScrollableY(boolean scrollableY) {
if(layout instanceof BorderLayout) {
this.scrollableY = false;
} else {
this.scrollableY = scrollableY;
}
}
/**
* The equivalent of calling both setScrollableY and setScrollableX
*
* @param scrollable whether the component should/could scroll on the
* X and Y axis
*
* @deprecated use setScrollableX and setScrollableY instead. This method is deprecated since it breeds confusion and is often misunderstood.
*/
public void setScrollable(boolean scrollable) {
setScrollableX(scrollable);
setScrollableY(scrollable);
}
/**
* {@inheritDoc}
*/
public void setCellRenderer(boolean cellRenderer) {
if (isCellRenderer() != cellRenderer) {
super.setCellRenderer(cellRenderer);
int size = getComponentCount();
for (int iter = 0; iter <
size; iter++) {
getComponentAt(iter).setCellRenderer(cellRenderer);
}
}
}
/**
* Determines the scroll increment size of this Container.
* This value is in use when the current foucs element within this Container
* is larger than this Container size.
*
* @param scrollIncrement the size in pixels.
*/
public void setScrollIncrement(int scrollIncrement) {
this.scrollIncrement = scrollIncrement;
}
/**
* Gets the Container scroll increment
*
* @return the scroll increment in pixels.
*/
public int getScrollIncrement() {
return scrollIncrement;
}
/**
* Finds the first focusable Component on this Container
*
* @return a focusable Component or null if not exists;
*/
public Component findFirstFocusable() {
int size = getComponentCount();
for (int iter = 0; iter < size; iter++) {
Component current = getComponentAt(iter);
if(current.isVisible()) {
if(current.isFocusable()){
return current;
}
if (current instanceof Container && !((Container)current).isBlockFocus() && ((Container)current).getLeadComponent() == null) {
Component cmp = ((Container)current).findFirstFocusable();
if(cmp != null){
return cmp;
}
}
}
}
return null;
}
/**
* Recusively focuses components for the lead component functionality
*/
private void setFocusLead(boolean f) {
int count = getComponentCount();
for (int i = 0; i < count; i++) {
Component c = getComponentAt(i);
if(c instanceof Container) {
((Container)c).setFocusLead(f);
}
c.setFocus(f);
if(f) {
c.fireFocusGained();
} else {
c.fireFocusLost();
}
}
}
/**
* {@inheritDoc}
*/
protected void dragInitiated() {
super.dragInitiated();
if(leadComponent != null) {
leadComponent.dragInitiated();
}
}
/**
* {@inheritDoc}
*/
protected void fireClicked() {
if(leadComponent != null) {
leadComponent.fireClicked();
} else {
super.fireClicked();
}
}
/**
* {@inheritDoc}
*/
protected boolean isSelectableInteraction() {
if(leadComponent != null) {
return leadComponent.isSelectableInteraction();
} else {
return super.isSelectableInteraction();
}
}
/**
* This method will recursively set all the Container chidrens to be
* enabled/disabled.
* If the Container is disabled and a child Component changed it's state to
* be enabled, the child Component will be treated as an enabled Component.
* @param enabled
*/
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
int count = getComponentCount();
for (int i = 0; i < count; i++) {
Component c = getComponentAt(i);
c.setEnabled(enabled);
}
}
/**
* This is a callback method for the peer component class
*/
void setLightweightMode(boolean l) {
int size = getComponentCount();
for(int iter = 0 ; iter < size ; iter++) {
getComponentAt(iter).setLightweightMode(l);
}
}
/**
* {@inheritDoc}
*/
protected int getGridPosY() {
int scroll = getScrollY();
int size = getComponentCount();
int bestRow = 0;
for(int iter = 0 ; iter < size ; iter++) {
Component c = getComponentAt(iter);
int y = c.getY();
if(Math.abs(scroll - y) < Math.abs(scroll - bestRow)) {
bestRow = y;
}
}
if(Math.abs(scroll - bestRow) > 2) {
return bestRow;
}
return scroll;
}
/**
* Returns false for the special case where a container has an opaque/flattened child that
* occupies its entire face
*/
private boolean shouldPaintContainerBackground() {
return !isObscuredByChildren();
}
/**
* {@inheritDoc}
*/
public void paintComponentBackground(Graphics g) {
if(isFlatten()) {
super.paintBackgrounds(g);
return;
}
if(shouldPaintContainerBackground()) {
super.paintComponentBackground(g);
}
}
@Override
protected void paintBackground(Graphics g) {
super.paintBackground(g);
}
@Override
protected void paintBorderBackground(Graphics g) {
super.paintBorderBackground(g);
}
/**
* {@inheritDoc}
*/
protected int getGridPosX() {
int scroll = getScrollX();
int size = getComponentCount();
int bestCol = 0;
for(int iter = 0 ; iter < size ; iter++) {
Component c = getComponentAt(iter);
int x = c.getX();
if(Math.abs(scroll - x) < Math.abs(scroll - bestCol)) {
bestCol = x;
}
}
if(Math.abs(scroll - bestCol) > 2) {
return bestCol;
}
return scroll;
}
/**
* This method blocks all children from getting focus
*
* @param blockFocus
*/
void setBlockFocus(boolean blockFocus) {
this.blockFocus = blockFocus;
}
/**
* Returns true if focus is blocked for this Container
*
* @return
*/
boolean isBlockFocus() {
return blockFocus;
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation. This method waits until the operation is completed before returning
*
* @param duration the duration in milliseconds for the animation
*/
public void animateHierarchyAndWait(final int duration) {
animateHierarchy(duration, true, 255, true);
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation.
*
* @param duration the duration in milliseconds for the animation
* @return the animation object that should be added to the animation manager
*/
public ComponentAnimation createAnimateHierarchy(final int duration) {
return animateHierarchy(duration, false, 255, false);
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
*/
public void animateHierarchy(final int duration) {
animateHierarchy(duration, false, 255, true);
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation. This method waits until the operation is completed before returning
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
*/
public void animateHierarchyFadeAndWait(final int duration, int startingOpacity) {
animateHierarchy(duration, true, startingOpacity, true);
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation.
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
* @return the animation object that should be added to the animation manager
*/
public ComponentAnimation createAnimateHierarchyFade(final int duration, int startingOpacity) {
return animateHierarchy(duration, false, startingOpacity, false);
}
/**
* Animates a pending hierarchy of components into place, this effectively replaces revalidate with
* a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
*/
public void animateHierarchyFade(final int duration, int startingOpacity) {
animateHierarchy(duration, false, startingOpacity, true);
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation. This method
* waits until the operation is completed before returning
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
*/
public void animateLayoutFadeAndWait(final int duration, int startingOpacity) {
animateLayout(duration, true, startingOpacity, true);
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation. This method
* waits until the operation is completed before returning
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
* @return the animation object that should be added to the animation manager
* @deprecated this was added by mistake!
*/
public ComponentAnimation createAnimateLayoutFadeAndWait(final int duration, int startingOpacity) {
return null;
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
*/
public void animateLayoutFade(final int duration, int startingOpacity) {
animateLayout(duration, false, startingOpacity, true);
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
* @param startingOpacity the initial opacity to give to the animated components
* @return the animation object that should be added to the animation manager
*/
public ComponentAnimation createAnimateLayoutFade(final int duration, int startingOpacity) {
return animateLayout(duration, false, startingOpacity, false);
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation. This method
* waits until the operation is completed before returning
*
* @param duration the duration in milliseconds for the animation
*/
public void animateLayoutAndWait(final int duration) {
animateLayout(duration, true, 255, true);
}
/**
*
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
* See:
*
*
*
*
* @param duration the duration in milliseconds for the animation
*/
public void animateLayout(final int duration) {
animateLayout(duration, false, 255, true);
}
/**
* Updates the tab indices in this container recursively. This method is used internally by
* layout managers when calculating the traversal order of components in a form.
* @param offset The starting tab index.
* @return The ending tab index (+1)
* @deprecated For internal use only.
*/
public int updateTabIndices(int offset) {
Container parent = this;
Layout l = parent.getActualLayout();
if (l.overridesTabIndices(parent)) {
return l.updateTabIndices(parent, offset);
}
int len = parent.getComponentCount();
int idx = offset;
for (int i=0; i
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
* See:
*
*
*
*
* @param duration the duration in milliseconds for the animation
* @return the animation object that should be added to the animation manager
*/
public ComponentAnimation createAnimateLayout(final int duration) {
return animateLayout(duration, false, 255, false);
}
/**
* {@inheritDoc}
*/
public void drop(Component dragged, int x, int y) {
int i = getComponentIndex(dragged);
if(i > -1) {
Component dest = getComponentAt(x, y);
if(dest != dragged) {
int destIndex = getComponentIndex(dest);
if(destIndex > -1 && destIndex != i) {
setComponentIndex(dragged,destIndex);
}
}
animateLayout(400);
} else {
Container oldParent = dragged.getParent();
if(oldParent != null) {
oldParent.removeComponent(dragged);
}
Component pos = getComponentAt(x, y);
i = getComponentIndex(pos);
if(i > -1) {
addComponent(i, dragged);
} else {
addComponent(dragged);
}
getComponentForm().animateHierarchy(400);
}
}
/**
* Creates a motion object for animation, allows subclasses to replace the motion type
* used in animations (currently defaults to ease-in).
*
* @param start start value
* @param destination destination value
* @param duration duration of animation
* @return motion object
*/
protected Motion createAnimateMotion(int start, int destination, int duration) {
return Motion.createEaseInMotion(start, destination, duration);
}
private Motion createAndStartAnimateMotion(int start, int destination, int duration) {
Motion m = createAnimateMotion(start, destination, duration);
m.start();
return m;
}
private void findComponentsInHierachy(Vector vec) {
int cc = getComponentCount();
for(int iter = 0 ; iter < cc ; iter++) {
Component c = getComponentAt(iter);
vec.addElement(c);
if(c.getClass() == Container.class) {
((Container)c).findComponentsInHierachy(vec);
}
}
}
/**
* Morph is similar to the replace functionality where a component might be replaced with
* a component that isn't within the container. However, unlike the replace functionality which
* uses a transition and assumes the position of the component (and is hence quite flexible) morph
* can move and resize the component. E.g. after entering text into a text field and pressing submit
* it can "morph" into a chat bubble located in a different part of the screen.
* It is the responsibility of the caller to remove the source component (if desired) and revalidate the
* container when the animation completes.
*
* @param source source component assumed to be within this container or one of its children
* @param destination the destination component
* @param duration the time the morph operation should take
* @param onCompletion invoked when the morphing completes
*/
public void morph(Component source, Component destination, int duration, Runnable onCompletion) {
morph(source, destination, duration, false, onCompletion);
}
/**
* Morph is similar to the replace functionality where a component might be replaced with
* a component that isn't within the container. However, unlike the replace functionality which
* uses a transition and assumes the position of the component (and is hence quite flexible) morph
* can move and resize the component. E.g. after entering text into a text field and pressing submit
* it can "morph" into a chat bubble located in a different part of the screen.
* It is the responsibility of the caller to remove the source component (if desired) and revalidate the
* container when the animation completes.
*
* @param source source component assumed to be within this container or one of its children
* @param destination the destination component
* @param duration the time the morph operation should take
*/
public void morphAndWait(Component source, Component destination, int duration) {
morph(source, destination, duration, true, null);
}
private void morph(Component source, Component destination, int duration, boolean wait, Runnable onCompletion) {
setShouldCalcPreferredSize(true);
enableLayoutOnPaint = false;
dontRecurseContainer = true;
int deltaX = getAbsoluteX();
int deltaY = getAbsoluteY();
int sourceX = source.getAbsoluteX() - deltaX;
int destX = destination.getAbsoluteX() - deltaX;
int sourceY = source.getAbsoluteY() - deltaY;
int destY = destination.getAbsoluteY() - deltaY;
final Motion[] xMotions = new Motion[] {
createAndStartAnimateMotion(sourceX, destX, duration),
createAndStartAnimateMotion(sourceX, destX, duration)
};
final Motion[] yMotions = new Motion[] {
createAndStartAnimateMotion(sourceY, destY, duration),
createAndStartAnimateMotion(sourceY, destY, duration)
};
final Motion[] wMotions = new Motion[] {
createAndStartAnimateMotion(source.getWidth(), destination.getWidth(), duration),
createAndStartAnimateMotion(source.getWidth(), destination.getWidth(), duration)
};
final Motion[] hMotions = new Motion[] {
createAndStartAnimateMotion(source.getHeight(), destination.getHeight(), duration),
createAndStartAnimateMotion(source.getHeight(), destination.getHeight(), duration)
};
MorphAnimation a = new MorphAnimation(this, duration, new Motion[][] {
xMotions, yMotions, wMotions, hMotions
});
a.opacity = new Motion[] {
createAndStartAnimateMotion(255, 0, duration),
createAndStartAnimateMotion(0, 255, duration)
};
a.animatedComponents = new Vector();
a.animatedComponents.addElement(source);
a.animatedComponents.addElement(destination);
a.dontRevalidate = true;
a.scrollTo = destination;
if(wait) {
getAnimationManager().addAnimationAndBlock(a);
} else {
if(onCompletion != null) {
getAnimationManager().addAnimation(a, onCompletion);
} else {
getAnimationManager().addAnimation(a);
}
}
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
*/
private ComponentAnimation animateHierarchy(final int duration, boolean wait, int opacity, boolean add) {
setShouldCalcPreferredSize(true);
enableLayoutOnPaint = false;
dontRecurseContainer = true;
Vector comps = new Vector();
findComponentsInHierachy(comps);
final int componentCount = comps.size();
int[] beforeX = new int[componentCount];
int[] beforeY = new int[componentCount];
int[] beforeW = new int[componentCount];
int[] beforeH = new int[componentCount];
final Motion[] xMotions = new Motion[componentCount];
final Motion[] yMotions = new Motion[componentCount];
final Motion[] wMotions = new Motion[componentCount];
final Motion[] hMotions = new Motion[componentCount];
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = (Component)comps.elementAt(iter);
beforeX[iter] = current.getX();
beforeY[iter] = current.getY();
beforeW[iter] = current.getWidth();
beforeH[iter] = current.getHeight();
}
layoutContainer();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = (Component)comps.elementAt(iter);
xMotions[iter] = createAnimateMotion(beforeX[iter], current.getX(), duration);
yMotions[iter] = createAnimateMotion(beforeY[iter], current.getY(), duration);
wMotions[iter] = createAnimateMotion(beforeW[iter], current.getWidth(), duration);
hMotions[iter] = createAnimateMotion(beforeH[iter], current.getHeight(), duration);
xMotions[iter].start();
yMotions[iter].start();
wMotions[iter].start();
hMotions[iter].start();
current.setX(beforeX[iter]);
current.setY(beforeY[iter]);
current.setWidth(beforeW[iter]);
current.setHeight(beforeH[iter]);
}
MorphAnimation a = new MorphAnimation(this, duration, new Motion[][] {
xMotions, yMotions, wMotions, hMotions
});
setAnimOpacity(opacity, 255, a, componentCount, duration);
a.animatedComponents = comps;
if(add) {
if(wait) {
getAnimationManager().addAnimationAndBlock(a);
} else {
getAnimationManager().addAnimation(a);
}
}
return a;
}
/**
* This method is the exact reverse of animateLayout, when completed it leaves the container in
* an invalid state. It is useful to invoke this in order to remove a component, transition to a
* different form or provide some other interaction. E.g.:
*
*
* @param duration the duration of the animation
* @param opacity the opacity to which the layout will reach, allows fading out the components
* @param callback if not null will be invoked when unlayouting is complete
*/
public void animateUnlayout(final int duration, int opacity, Runnable callback) {
animateUnlayout(duration, false, opacity, callback, true);
}
/**
* This method is the exact reverse of animateLayoutAndWait, when completed it leaves the container in
* an invalid state. It is useful to invoke this in order to remove a component, transition to a
* different form or provide some other interaction. E.g.:
*
*
* @param duration the duration of the animation
* @param opacity the opacity to which the layout will reach, allows fading out the components
*/
public void animateUnlayoutAndWait(final int duration, int opacity) {
animateUnlayout(duration, true, opacity, null, true);
}
/**
* This method is the exact reverse of createAnimateLayout, when animation is completed it leaves the container in
* an invalid state. It is useful to invoke this in order to remove a component, transition to a
* different form or provide some other interaction. E.g.:
*
* @param duration the duration of the animation
* @param opacity the opacity to which the layout will reach, allows fading out the components
* @return the animation object that should be added to the animation manager
*/
public ComponentAnimation createAnimateUnlayout(int duration, int opacity, Runnable callback) {
return animateUnlayout(duration, false, opacity, callback, false);
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
*/
private ComponentAnimation animateUnlayout(final int duration, boolean wait, int opacity, Runnable callback, boolean add) {
setShouldCalcPreferredSize(true);
enableLayoutOnPaint = false;
final int componentCount = getComponentCount();
int[] beforeX = new int[componentCount];
int[] beforeY = new int[componentCount];
int[] beforeW = new int[componentCount];
int[] beforeH = new int[componentCount];
final Motion[] xMotions = new Motion[componentCount];
final Motion[] yMotions = new Motion[componentCount];
final Motion[] wMotions = new Motion[componentCount];
final Motion[] hMotions = new Motion[componentCount];
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = getComponentAt(iter);
beforeX[iter] = current.getX();
beforeY[iter] = current.getY();
beforeW[iter] = current.getWidth();
beforeH[iter] = current.getHeight();
}
layoutContainer();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = getComponentAt(iter);
xMotions[iter] = createAnimateMotion(current.getX(), beforeX[iter], duration);
yMotions[iter] = createAnimateMotion(current.getY(), beforeY[iter], duration);
wMotions[iter] = createAnimateMotion(current.getWidth(), beforeW[iter], duration);
hMotions[iter] = createAnimateMotion(current.getHeight(), beforeH[iter], duration);
xMotions[iter].start();
yMotions[iter].start();
wMotions[iter].start();
hMotions[iter].start();
}
MorphAnimation a = new MorphAnimation(this, duration, new Motion[][] {
xMotions, yMotions, wMotions, hMotions
});
setAnimOpacity(255, opacity, a, componentCount, duration);
a.dontRevalidate = true;
if (add) {
if(wait) {
getAnimationManager().addAnimationAndBlock(a);
} else {
if(callback != null) {
getAnimationManager().addUIMutation(this, a, callback);
} else {
getAnimationManager().addUIMutation(this, a);
}
}
}
return a;
}
/**
* Animates a pending layout into place, this effectively replaces revalidate with a more visual form of animation
*
* @param duration the duration in milliseconds for the animation
*/
private ComponentAnimation animateLayout(final int duration, boolean wait, int opacity, boolean addAnimation) {
// this happens for some reason
Form f = getComponentForm();
if(f == null) {
return null;
}
setShouldCalcPreferredSize(true);
enableLayoutOnPaint = false;
final int componentCount = getComponentCount();
int[] beforeX = new int[componentCount];
int[] beforeY = new int[componentCount];
int[] beforeW = new int[componentCount];
int[] beforeH = new int[componentCount];
final Motion[] xMotions = new Motion[componentCount];
final Motion[] yMotions = new Motion[componentCount];
final Motion[] wMotions = new Motion[componentCount];
final Motion[] hMotions = new Motion[componentCount];
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = getComponentAt(iter);
beforeX[iter] = current.getX();
beforeY[iter] = current.getY();
beforeW[iter] = current.getWidth();
beforeH[iter] = current.getHeight();
}
layoutContainer();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component current = getComponentAt(iter);
xMotions[iter] = createAnimateMotion(beforeX[iter], current.getX(), duration);
yMotions[iter] = createAnimateMotion(beforeY[iter], current.getY(), duration);
wMotions[iter] = createAnimateMotion(beforeW[iter], current.getWidth(), duration);
hMotions[iter] = createAnimateMotion(beforeH[iter], current.getHeight(), duration);
xMotions[iter].start();
yMotions[iter].start();
wMotions[iter].start();
hMotions[iter].start();
current.setX(beforeX[iter]);
current.setY(beforeY[iter]);
current.setWidth(beforeW[iter]);
current.setHeight(beforeH[iter]);
}
MorphAnimation a = new MorphAnimation(this, duration, new Motion[][] {
xMotions, yMotions, wMotions, hMotions
});
setAnimOpacity(opacity, 255, a, componentCount, duration);
if(addAnimation) {
if(wait) {
getAnimationManager().addAnimationAndBlock(a);
} else {
getAnimationManager().addUIMutation(this, a);
}
} else {
a.dontRevalidate = true;
}
return a;
}
private void setAnimOpacity(int source, int dest, MorphAnimation a, int componentCount, int duration) {
if(source != dest) {
a.opacity = new Motion[componentCount];
for(int iter = 0 ; iter < componentCount ; iter++) {
a.opacity[iter] = createAndStartAnimateMotion(source, dest, duration);
}
}
}
/**
* Gets the child components of this Container as a List. Using {@literal true} as the
* argument provides a way to obtain all of the children, including children whose full
* addition is pending while an animation is in progress.
*
* Animation Discussion: If children are added or removed from a Container
* while its containing Form has an animation in progress, the insertion/deletion isn't complete
* until after the animation is finished. Most methods to interact with a container's children
* won't see these pending changes until that time. E.g.:
*
* {@code
* // Assume an animation is in progress on the form containing cnt.
* Label lbl = new Label("Test");
* int len = cnt.getComponentCount(); // 0
* cnt.addComponent(lbl);
* int lenAfter = cnt.getComponentCount(); // 0
* cnt.contains(lbl); // true
* cnt.getChildrenAsList(true).size(); // 1
* cnt.getChildrenAsList(false).size(); // 0
*
* Button btn = new Button("Press me");
* cnt.addComponent(btn);
* cnt.getComponentCount(); // 0
* cnt.getChildrenAsList(true).size(); // 2
* cnt.removeComponent(btn);
* cnt.getComponentCount(); // 0
* cnt.getChildrenAsList(true).size(); // 1
*
* }
* @param includeQueued True to reflect queued inserts and removals while an animation is in progress.
* @return A list including all of the children of this container.
* @see #iterator(boolean)
*/
public java.util.List getChildrenAsList(boolean includeQueued) {
if (includeQueued) {
java.util.ArrayList out = new java.util.ArrayList();
out.addAll(components);
if (changeQueue != null) {
for (QueuedChange change : changeQueue) {
switch (change.type) {
case QueuedChange.TYPE_INSERT:
QueuedInsertion insert = (QueuedInsertion)change;
int index = insert.index;
if(insert.index == Integer.MAX_VALUE) {
index = out.size();
}
out.add(index, change.component);
break;
case QueuedChange.TYPE_REMOVE:
out.remove(change.component);
break;
}
}
};
return out;
} else {
java.util.ArrayList out = new java.util.ArrayList();
out.addAll(components);
return out;
}
}
/**
* Obtains an iterator that iterates over the children of this container. If argument is true,
* then the iteratator will include queued insertions/deletions while an animation is in progress.
* @param includeQueued True to include queued component insertions and removals while animation is in progress.
* @return An iterator that iterates over the children of this component.
* @see #iterator()
* @see #getChildrenAsList(boolean)
*/
public Iterator iterator(boolean includeQueued) {
if (includeQueued) {
return getChildrenAsList(includeQueued).iterator();
} else {
return iterator();
}
}
/**
* Part of the Iterable interface allowing us to do a for-each loop on Container
* @return the iterator of the components
*/
public Iterator iterator() {
return components.iterator();
}
static class TransitionAnimation extends ComponentAnimation {
private Transition t;
private Container thisContainer;
int growSpeed;
int layoutAnimationSpeed;
Vector animatedComponents;
Motion[] opacity;
boolean dontRevalidate;
private boolean started = false;
private boolean inProgress = true;
private Component current;
private Component next;
private Form parent;
private boolean destroyed;
TransitionAnimation(Container thisContainer, Component current, Component next, Transition t) {
this.t = t;
this.next = next;
this.current = current;
this.thisContainer = thisContainer;
this.parent = thisContainer.getComponentForm();
}
public boolean isInProgress() {
return inProgress;
}
public void updateState() {
if(destroyed) {
return;
}
if (!started) {
t.init(current, next);
if(current != null) {
current.setLightweightMode(true);
}
if(next != null) {
next.setLightweightMode(true);
}
t.initTransition();
started = true;
if (thisContainer.cmpTransitions == null) {
thisContainer.cmpTransitions = new Vector();
}
thisContainer.cmpTransitions.addElement(this);
}
inProgress = t.animate();
if (!inProgress) {
thisContainer.cmpTransitions.removeElement(this);
destroy();
thisContainer.repaint();
} else {
Display.getInstance().repaint(t);
}
}
@Override
public void flush() {
destroy();
}
public void destroy() {
if(destroyed) {
return;
}
destroyed = true;
next.setParent(null);
thisContainer.replace(current, next, growSpeed > 0 || layoutAnimationSpeed > 0);
//release the events blocking
t.cleanup();
if (current != null) {
current.setLightweightMode(false);
}
if (next != null) {
next.setLightweightMode(false);
}
if(thisContainer.cmpTransitions != null && thisContainer.cmpTransitions.size() == 0 && growSpeed > -1){
if(growSpeed > 0) {
current.growShrink(growSpeed);
} else {
if (layoutAnimationSpeed <= 0 && !dontRevalidate) {
if (parent != null) {
parent.revalidate();
}
}
}
}
inProgress = false;
}
}
static class MorphAnimation extends ComponentAnimation {
private long startTime;
private int duration;
private Transition t;
private Container thisContainer;
private boolean finished = false;
private Motion[][] motions;
Runnable onFinish;
int growSpeed;
int layoutAnimationSpeed;
Vector animatedComponents;
Motion[] opacity;
boolean dontRevalidate;
private Component scrollTo;
public MorphAnimation(Container thisContainer, int duration, Motion[][] motions) {
startTime = System.currentTimeMillis();
this.duration = duration;
if(Motion.isSlowMotion()) {
this.duration *= 50;
}
this.thisContainer = thisContainer;
this.motions = motions;
}
@Override
public boolean isInProgress() {
return !finished;
}
@Override
public void flush() {
for(Motion[] mm : motions) {
for(Motion m : mm) {
if(m != null) {
m.finish();
}
}
}
updateState();
}
@Override
protected void updateState() {
if(animatedComponents != null) {
int componentCount = animatedComponents.size();
for(int iter = 0 ; iter < componentCount ; iter++) {
Component currentCmp = (Component)animatedComponents.elementAt(iter);
currentCmp.setX(motions[0][iter].getValue());
currentCmp.setY(motions[1][iter].getValue());
currentCmp.setWidth(motions[2][iter].getValue());
currentCmp.setHeight(motions[3][iter].getValue());
if(opacity != null) {
currentCmp.getStyle().setOpacity(opacity[iter].getValue(), false);
}
}
} else {
int componentCount = thisContainer.getComponentCount();
if(motions != null){
componentCount = Math.min(motions[0].length, componentCount);
}
for(int iter = 0 ; iter < componentCount ; iter++) {
Component currentCmp = thisContainer.getComponentAt(iter);
// this might happen if a container was replaced during animation
if(currentCmp == null) {
continue;
}
currentCmp.setX(motions[0][iter].getValue());
currentCmp.setY(motions[1][iter].getValue());
currentCmp.setWidth(motions[2][iter].getValue());
currentCmp.setHeight(motions[3][iter].getValue());
if(opacity != null) {
currentCmp.getStyle().setOpacity(opacity[iter].getValue(), false);
}
}
}
if(scrollTo != null) {
boolean s = thisContainer.isSmoothScrolling();
thisContainer.setSmoothScrolling(false);
thisContainer.scrollComponentToVisible(scrollTo);
thisContainer.setSmoothScrolling(s);
}
thisContainer.repaint();
if(System.currentTimeMillis() - startTime >= duration) {
enableLayoutOnPaint = true;
thisContainer.dontRecurseContainer = false;
Form f = thisContainer.getComponentForm();
finished = true;
if(f == null) {
return;
}
if(!dontRevalidate) {
f.revalidate();
}
}
}
}
}