/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine 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. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.ui;
import java.util.*;
import totalcross.sys.*;
import totalcross.ui.event.*;
import totalcross.ui.gfx.*;
/**
* ScrollContainer is a container with a horizontal only, vertical only, both or no
* ScrollBars, depending on the control positions.
* The default unit scroll is an Edit's height (for the vertical
* scrollbar), and the width of '@' (for the horizontal scrollbar).
* <p>
* <b>Caution</b>: you must not use RIGHT, BOTTOM, CENTER and FILL when setting the control bounds,
* unless you disable the corresponding ScrollBar! The only exception to this is to use FILL on the control's height,
* which is allowed.
* <p>
* Here is an example showing how it can be used:
*
* <pre>
* public class MyProgram extends MainWindow
* {
* ScrollContainer sc;
*
* public void initUI()
* {
ScrollContainer sc;
add(sc = new ScrollContainer());
sc.setBorderStyle(BORDER_SIMPLE);
sc.setRect(LEFT+10,TOP+10,FILL-20,FILL-20);
int xx = new Label("Name99").getPreferredWidth()+2; // edit's alignment
for (int i =0; i < 100; i++)
{
sc.add(new Label("Name"+i),LEFT,AFTER);
sc.add(new Edit("@@@@@@@@@@@@@@@@@@@@"),xx,SAME);
if (i % 3 == 0) sc.add(new Button("Go"), AFTER+2,SAME,PREFERRED,SAME);
}
* }
*}
* </pre>
*/
public class ScrollContainer extends Container implements Scrollable
{
/** Returns the scrollbar for this ScrollContainer. With it, you can directly
* set its parameters, like blockIncrement, unitIncrement and liveScrolling.
* But be careful, don't mess with the minimum, maximum and visibleItems.
*/
public ScrollBar sbH,sbV;
/** The Flick object listens and performs flick animations on PenUp events when appropriate. */
protected Flick flick;
protected ClippedContainer bag;
protected Container bag0; // used to make sure that the clipping will work
boolean changed;
protected int lastV=0, lastH=0; // eliminate duplicate events
/** Set to true, to make the surrounding container shrink to its size. */
public boolean shrink2size;
private boolean isScrolling;
private boolean scScrolled;
private Object lastScrolled;
/** Automatically scrolls the container when an item is clicked.
* @see #hsIgnoreAutoScroll
*/
public boolean autoScroll;
/** Defines a list of classes that will make autoScroll be ignored.
* Use it like:
* <pre>
* ScrollContainer.hsIgnoreAutoScroll.add(totalcross.ui.SpinList.class);
* ...
* </pre>
* This is useful if such class usually requires more than one press to have a value defined.
* @see #autoScroll
*/
public static HashSet<Class<?>> hsIgnoreAutoScroll = new HashSet<Class<?>>(5);
/** Standard constructor for a new ScrollContainer, with both scrollbars enabled.
*/
public ScrollContainer()
{
this(true);
}
/** Constructor used to specify when both scrollbars are enabled or not. */
public ScrollContainer(boolean allowScrollBars)
{
this(allowScrollBars, allowScrollBars);
}
/** Constructor used to specify when each scrollbar is enabled or not.
* By disabling the horizontal scrollbar, you can use RIGHT and CENTER on the x parameter of a control that is added.
* By disabling the vertical scrollbar, you can use BOTTOM and CENTER on the y parameter of a control that is added.
* @since TotalCross 1.27
*/
public ScrollContainer(boolean allowHScrollBar, boolean allowVScrollBar)
{
super.add(bag0 = new Container());
bag0.add(bag = new ClippedContainer());
bag.ignoreOnAddAgain = bag.ignoreOnRemove = true;
bag0.ignoreOnAddAgain = bag0.ignoreOnRemove = true;
bag.setRect(0,0,4000,20000); // set an arbitrary size
bag.setX = SETX_NOT_SET; // ignore this setX and use the next one
if (allowHScrollBar)
{
sbH = Settings.fingerTouch ? new ScrollPosition(ScrollBar.HORIZONTAL) : new ScrollBar(ScrollBar.HORIZONTAL);
sbH.setLiveScrolling(true);
sbH.setMaximum(0);
}
if (allowVScrollBar)
{
sbV = Settings.fingerTouch ? new ScrollPosition(ScrollBar.VERTICAL) : new ScrollBar(ScrollBar.VERTICAL);
sbV.setLiveScrolling(true);
sbV.setMaximum(0);
}
if (Settings.fingerTouch)
flick = new Flick(this);
}
public boolean flickStarted()
{
return true;//isScrolling; // flick1.robot fails with this
}
public void flickEnded(boolean atPenDown)
{
bag.releaseScreenShot();
}
public boolean canScrollContent(int direction, Object target)
{
if (direction == 4)
direction = 4;
boolean ret = false;
if (Settings.fingerTouch)
switch (direction)
{
case DragEvent.UP : ret = sbV != null && sbV.value > sbV.minimum; break;
case DragEvent.DOWN : ret = sbV != null && (sbV.value + sbV.visibleItems) < sbV.maximum; break;
case DragEvent.LEFT : ret = sbH != null && sbH.value > sbH.minimum; break;
case DragEvent.RIGHT: ret = sbH != null && (sbH.value + sbH.visibleItems) < sbH.maximum; break;
}
return ret;
}
public boolean scrollContent(int dx, int dy, boolean fromFlick)
{
boolean scrolled = false;
if (dx != 0 && sbH != null)
{
int oldValue = sbH.value;
sbH.setValue(oldValue + dx);
lastH = sbH.value;
if (oldValue != lastH)
{
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT - lastH, KEEP,KEEP,KEEP);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
scrolled = true;
if (!fromFlick) sbH.tempShow();
}
}
if (dy != 0 && sbV != null)
{
int oldValue = sbV.value;
sbV.setValue(oldValue + dy);
lastV = sbV.value;
if (oldValue != lastV)
{
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(KEEP, TOP - lastV, KEEP, KEEP);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
scrolled = true;
if (!fromFlick) sbV.tempShow();
}
}
if (scrolled)
{
Window.needsPaint = true;
return true;
}
else
return false;
}
public int getScrollPosition(int direction)
{
return direction == DragEvent.LEFT || direction == DragEvent.RIGHT ? lastH : lastV;
}
/** Adds a child control to the bag container. */
public void add(Control control)
{
changed = true;
bag.add(control);
}
/** Adds a control to the ScrollContainer itself. Used internally. */
void addToSC(Control c)
{
bag0.add(c);
}
/**
* Removes a child control from the bag container.
*/
public void remove(Control control)
{
changed = true;
bag.remove(control);
}
protected void onBoundsChanged(boolean screenChanged)
{
bag0.setRect(LEFT, TOP, FILL, FILL, null, screenChanged);
if (sbH == null && sbV == null && shrink2size)
{
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT, TOP, FILL, FILL, null, screenChanged);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
else if (sbH == null || sbV == null)
{
int w = FILL - (sbV != null && !Settings.fingerTouch ? sbV.getPreferredWidth() : 0); // guich@tc152: set original size to the parent's one, so user can use FILL and PARENTSIZE
int h = FILL - (sbH != null && !Settings.fingerTouch ? sbH.getPreferredHeight() : 0);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT, TOP, w, h, null, screenChanged);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
}
protected void onColorsChanged(boolean colorsChanged)
{
super.onColorsChanged(colorsChanged);
if (colorsChanged)
{
bag.setBackForeColors(backColor, foreColor);
bag0.setBackForeColors(backColor, foreColor);
if (sbV != null)
sbV.setBackForeColors(backColor, foreColor);
if (sbH != null)
sbH.setBackForeColors(backColor,foreColor);
}
}
/** This method resizes the control to the needed bounds, based on added childs.
* Must be called if you're controlling reposition by your own, after you repositioned the controls inside of it. */
public void resize()
{
int maxX = 0;
int maxY = 0;
boolean hasFillH = false;
for (Control child = bag.children; child != null; child = child.next)
{
int m = child.x+child.width;
if (m > maxX)
maxX = m;
int hh = child.height;
if (!hasFillH && sbV != null && (FILL-RANGE) <= child.setH && child.setH <= (FILL+RANGE)) // if control has fill on the height, don't take it into consideration
{
hasFillH = true;
hh = 0;
}
m = child.y+hh;
if (m > maxY)
maxY = m;
}
if (hasFillH) // now resize the height
{
maxY = getClientRect().height;
for (Control child = bag.children; child != null; child = child.next)
if ((FILL-RANGE) <= child.setH && child.setH <= (FILL+RANGE))
{
child.height = maxY-child.y + (uiAdjustmentsBasedOnFontHeightIsSupported ? (child.setH-FILL)*fmH/100 : (child.setH-FILL));
child.onBoundsChanged(true);
}
}
resize(maxX == 0 ? FILL : maxX, maxY == 0 ? PREFERRED : maxY);
}
/** This method resizes the control to the needed bounds, based on the given maximum width and heights. */
/** This method resizes the control to the needed bounds, based on the given maximum width and heights. */
public void resize(int maxX, int maxY)
{
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(bag.x, bag.y, maxX, maxY);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
if (sbV != null)
super.remove(sbV);
if (sbH != null)
super.remove(sbH);
// check if we need horizontal or vertical or both scrollbars
boolean needX = false, needY = false, changed=false;
Rect r = getClientRect();
int availX = r.width;
int availY = r.height;
boolean finger = ScrollPosition.AUTO_HIDE &&
((sbH != null && sbH instanceof ScrollPosition) ||
(sbV != null && sbV instanceof ScrollPosition));
if (sbH != null || sbV != null)
do
{
changed = false;
if (!needY && maxY > availY)
{
changed = needY = true;
if (finger && sbH != null && sbV != null) availX -= sbV.getPreferredWidth();
}
if (!needX && maxX > availX) // do we need an horizontal scrollbar?
{
changed = needX = true;
if (finger && sbV != null && sbH != null) availY -= sbH.getPreferredHeight(); // remove the horizbar area from the avail Y area
}
} while (changed);
if (sbH != null || sbV != null || !shrink2size)
bag0.setRect(r.x,r.y,r.width-(!finger && needY && sbV != null ? sbV.getPreferredWidth() : 0), r.height-(!finger && needX && sbH != null ? sbH.getPreferredHeight() : 0));
else
{
bag0.setRect(r.x,r.y,maxX,maxY);
setRect(this.x,this.y,maxX,maxY);
}
if (needX && sbH != null)
{
super.add(sbH);
sbH.setMaximum(maxX);
sbH.setVisibleItems(bag0.width);
sbH.setRect(LEFT,BOTTOM,FILL-(!finger && needY?sbV.getPreferredWidth():0),PREFERRED);
sbH.setUnitIncrement(flick != null && flick.scrollDistance > 0 ? flick.scrollDistance : fm.charWidth('@'));
lastH = 0;
}
else if (sbH != null) sbH.setMaximum(0); // kmeehl@tc100: drag-scrolling depends on this to determine the bounds
if (needY && sbV != null)
{
super.add(sbV);
sbV.setMaximum(maxY);
sbV.setVisibleItems(bag0.height);
sbV.setRect(RIGHT,TOP,PREFERRED,FILL);
sbV.setUnitIncrement(flick != null && flick.scrollDistance > 0 ? flick.scrollDistance : fmH+Edit.prefH);
lastV = 0;
}
else if (sbV != null) sbV.setMaximum(0); // kmeehl@tc100: drag-scrolling depends on this to determine the bounds
Window.needsPaint = true;
}
/** Override this method to return the correct scroll distance. Defaults to the container's width. */
public int getScrollDistance()
{
return this.width;
}
public void reposition()
{
int vx = bag.x, vy = bag.y; // keep position when changing size
int curPage = flick != null && flick.pagepos != null ? flick.pagepos.getPosition() : 0;
super.reposition();
resize();
if (flick != null && flick.scrollDistance != 0)
flick.setScrollDistance(getScrollDistance());
if (curPage != 0)
scrollToPage(curPage);
else
{
if (sbH != null)
sbH.setValue(-(bag.x = sbH.maximum == 0 ? 0 : vx));
if (sbV != null)
sbV.setValue(-(bag.y = sbV.maximum == 0 ? 0 : vy)); // if we're scrolled but we don't need scroll, move to origin
}
}
/**
* Returns the preferred width AFTER the resize method was called. If the ScrollBars are disabled, returns the
* maximum size of the container to hold all controls.
*/
public int getPreferredWidth()
{
int horizontalMax = sbH == null ? 0 : sbH.maximum;
return sbV == null ? bag.width : horizontalMax + (sbV.maximum == 0 ? 0 : sbV.getPreferredWidth());
}
/**
* Returns the preferred height AFTER the resize method was called. If the ScrollBars are disabled, returns the
* maximum size of the container to hold all controls.
*/
public int getPreferredHeight()
{
int verticalMax = sbV == null ? 0 : sbV.maximum;
return sbH == null ? bag.height : verticalMax + (sbH.maximum == 0 ? 0 : sbH.getPreferredWidth());
}
public void onPaint(Graphics g)
{
if (changed)
{
resize();
changed = false;
}
super.onPaint(g);
}
public void onEvent(Event event)
{
switch (event.type)
{
case ControlEvent.PRESSED:
if (event.target == sbV && sbV.value != lastV)
{
lastV = sbV.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(bag.x,TOP-lastV,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
else
if (event.target == sbH && sbH.value != lastH)
{
lastH = sbH.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT-lastH,bag.y,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
break;
case PenEvent.PEN_DOWN:
scScrolled = false;
break;
case PenEvent.PEN_DRAG_START:
if (Settings.optimizeScroll && ((DragEvent)event).direction != 0 && isFirstBag((Control)event.target) && bag.offscreen == null && Settings.fingerTouch && bag.width < 4096 && bag.height < 4096) // 4k is the texture's limit on most devices
bag.takeScreenShot();
break;
case PenEvent.PEN_DRAG_END:
if (flick != null && Flick.currentFlick == null && bag.offscreen != null)
bag.releaseScreenShot();
break;
case PenEvent.PEN_DRAG:
if (event.target == sbV || event.target == sbH) break;
if (Settings.fingerTouch)
{
Window w = getParentWindow();
if (w != null && w._focus == w.focusOnPenUp)
break;
DragEvent de = (DragEvent)event;
int dx = -de.xDelta;
int dy = -de.yDelta;
if (isScrolling)
{
scrollContent(dx, dy, true);
event.consumed = true;
//Event.clearQueue(PenEvent.PEN_DRAG);
}
else
{
int direction = DragEvent.getInverseDirection(de.direction);
if (!flick.isValidDirection(direction))
break;
if (canScrollContent(direction, de.target) && scrollContent(dx, dy, true))
event.consumed = isScrolling = scScrolled = true;
}
}
break;
case PenEvent.PEN_UP:
isScrolling = false;
if (autoScroll && event.target instanceof Control && event.target != lastScrolled && !hsIgnoreAutoScroll.contains(event.target.getClass()) && ((Control)event.target).isChildOf(this) && !((Control)event.target).hadParentScrolled())
{
Control c = (Control)event.target;
lastScrolled = c;
Rect r = c.getAbsoluteRect();
boolean scrolled = false;
if (sbV != null)
{
int k = this.height/3;
r.y -= this.getAbsoluteRect().y;
if (r.y > 2*k)
scrolled = scrollContent(0, k,false);
else
if (r.y2() < k)
scrolled = scrollContent(0,-k,false);
}
if (sbH != null && !scrolled)
{
int k = this.width/3;
r.x -= this.getAbsoluteRect().x;
if (r.x > 2*k)
scrollContent(k,0,false);
else
if (r.x2() < k)
scrollContent(-k,0,false);
}
}
break;
case ControlEvent.HIGHLIGHT_IN:
if (event.target != this)
scrollToControl((Control)event.target);
break;
}
}
private boolean isFirstBag(Control c)
{
for (; c != null; c = c.parent)
if (c instanceof ClippedContainer)
return c == bag;
return false;
}
/** Scrolls to the given page, which is the flick's scrollDistance (if set), or the control's height.
* @since TotalCross 1.53
*/
public void scrollToPage(int p)
{
int pageH = flick != null && flick.scrollDistance != 0 ? flick.scrollDistance : this.height;
int val = (p-1) * pageH;
if (sbH != null)
{
lastH = sbH.value;
sbH.setValue(val);
if (lastH != sbH.value)
{
lastH = sbH.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT-lastH,bag.y,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
}
else
{
lastV = sbV.value;
sbV.setValue(val);
if (lastV != sbV.value)
{
lastV = sbV.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(bag.x,TOP-lastV,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
}
if (flick != null && flick.pagepos != null)
flick.pagepos.setPosition(p);
}
/** Scrolls a page to left or right. Works only if it has a flick and a page position.
*/
public void scrollPage(boolean left)
{
int curPage = flick != null && flick.pagepos != null ? flick.pagepos.getPosition() : 0;
scrollToPage(left ? curPage-1 : curPage+1);
}
/** Scrolls to the given control. */
public void scrollToControl(Control c) // kmeehl@tc100
{
if (c != null && (sbH != null || sbV != null))
{
Rect r = c.getRect();
Control f = c.parent;
while (f.parent != this)
{
r.x += f.x;
r.y += f.y;
f = f.parent;
if (f == null)
return;// either c is not in this container, or it has since been removed from the UI
}
// horizontal
if (sbH != null && (r.x < 0 || r.x2() > bag0.width))
{
lastH = sbH.value;
int val = lastH + (r.x <= 0 || r.width > bag0.width ? r.x : (r.x2()-bag0.width));
if (val < sbH.minimum)
val = sbH.minimum;
sbH.setValue(val);
if (lastH != sbH.value)
{
lastH = sbH.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(LEFT-lastH,bag.y,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
}
// vertical
if (sbV != null && (r.y < 0 || r.y2() > bag0.height))
{
lastV = sbV.value;
int val = lastV + (r.y <= 0 || r.height > bag0.height ? r.y : (r.y2() - bag0.height));
if (val < sbV.minimum)
val = sbV.minimum;
sbV.setValue(val);
if (lastV != sbV.value)
{
lastV = sbV.value;
bag.uiAdjustmentsBasedOnFontHeightIsSupported = false;
bag.setRect(bag.x,TOP-lastV,bag.width,bag.height);
bag.uiAdjustmentsBasedOnFontHeightIsSupported = true;
}
}
}
}
public void setBorderStyle(byte border)
{
if (shrink2size)
bag.setBorderStyle(border);
else
super.setBorderStyle(border);
}
public Flick getFlick()
{
return flick;
}
public boolean wasScrolled()
{
return scScrolled;
}
/**
* Removes all controls from the ScrollContainer.
*/
public void removeAll()
{
bag.removeAll();
}
/** Returns the children of the bag. If you call ScrollContainer.getChildren, it will not return
* the controls added to the ScrollContainer, since they are actually added to the bag.
* @since TotalCross 1.5
*/
public Control[] getBagChildren()
{
return bag.getChildren();
}
public void onFontChanged()
{
bag.setFont(font);
}
public Control moveFocusToNextControl(Control control, boolean forward) // guich@tc125_26
{
return bag.moveFocusToNextControl(control, forward);
}
}