/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2004 Trev Quang Nguyen *
* Copyright (C) 2005-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.tree;
import totalcross.sys.*;
import totalcross.ui.*;
import totalcross.ui.event.*;
import totalcross.ui.gfx.*;
import totalcross.ui.image.*;
import totalcross.util.*;
/**
* This class is a simple implementation of a tree widget. Since it's
* natural to render the tree in rows, this class borrows most of the code from ListBox.
* Features:
* <ul>
* <li> similar to Microsoft Windows Explorer tree
* <li> horizontal and vertical scrolling
* <li> allows setting of folder and leaf icons.
* <li> expands and collapse of folder
* <li> allowsChildren flag to determine if the node is a leaf or folder
* <li> delete, insert, and modify (user object) of a node
* <li> clicking on leaf node will swap leaf icon (like hyperlink)
* <li> allows creation of tree to show or hide root node.
* </ul>
* You should use TreeModel class to modify the tree after.
* Here's a sample:
* <pre>
* TreeModel tmodel = new TreeModel();
* Tree tree = new Tree(tmodel);
* add(tree,LEFT,TOP,FILL,FILL);
* Node root = new Node("Tree");
* tmodel.setRoot(root);
* Node n;
* root.add(n = new Node("Branch1"));
* n.add(new Node("SubBranch1"));
* n.add(new Node("SubBranch2"));
* </pre>
* You can also see the FileChooserBox control and FileChooserTest (in UIGadgets sample).
*
* @see Node#userObject
*/
public class Tree extends Container implements PressListener, PenListener, KeyListener, Scrollable
{
public static final int SCROLLBAR_ALWAYS = 0;
public static final int SCROLLBAR_AS_NEEDED = 1;
public static final int SCROLLBAR_NEVER = 2;
public static final int ICON_PLUS = 0;
public static final int ICON_MINUS = 1;
public static final int ICON_OPEN = 2;
public static final int ICON_CLOSE = 3;
public static final int ICON_FILE = 4;
/** Set to true to allow multiple selections using a Check drawn before the nodes.
* @since TotalCross 1.15
*/
public boolean multipleSelection; // guich@tc115_4
/** Set to false to only expand or collapse if you click on the +- buttons.
* @since TotalCross 1.2
*/
public boolean expandClickingOnText = true; // guich@tc120_15
/** If true, all Tree will have the selection bar drawn in
* the full width instead of the selected's text width
* @since TotalCross 1.27
*/
public boolean useFullWidthOnSelection; // guich@tc125_29
/** The Flick object listens and performs flick animations on PenUp events when appropriate. */
protected Flick flick;
protected Image imgPlus; // the expand icon "-"
protected Image imgMinus; // the expand icon "+"
protected Image imgOpen; // the open folder icon
protected Image imgClose; // the close folder icon
protected Image imgFile; // the unvisited file icon
protected ScrollBar vbar; // vertical scrollbar
protected ScrollBar hbar; // horizontal scrollbar
protected TreeModel model; // holds the (original) node tree structure
protected Vector items = new Vector(); // hold the nodes to be drawn
protected int offset; // the vertical offset
protected int hsOffset; // the horizontal offset
protected int selectedIndex = -1; // the selected index
protected int itemCount; // the vertical scrollbar maximum (size of items vector)
protected int hsCount; // the horizontal scrollbar maximum
protected int visibleItems; // the first visible item display
protected int btnX; // the vertical scrollbar's x position
private boolean showRoot; // flag to show the root node or hide it.
private boolean allowsChildren = true; // flag to use the node's allowsChildren to determine
// if the node is a leaf or not
private int fColor = -1;
private int bgColor0 = -1;
private int bgColor1 = -1;
private int cursorColor = -1;
private int fourColors[] = new int[4];
private int imgPlusSize; // width of collapse[imgPlus] icon
private int imgOpenW; // width of folder [imgOpen] icon
private int imgOpenH; // height of folder [imgOpen] icon
private static final int hline = 3; // number of pixel used to draw horizontal line (from
// plus icon to gap between plus icon and folder or leaf icon)
private static final int gap = 1; // the number of space(in pixel) between the plus icon
// and the folder or leaf icon.
private int hbarPolicy = SCROLLBAR_AS_NEEDED;
private boolean showIcons = true;
private int x0;
private static Image imgOpenDefault,imgCloseDefault,imgFileDefault;
private boolean isScrolling;
private int lastV=-10000000, lastH=-10000000; // eliminate duplicate events
/** @deprecated Use setLineHeight and getLineHeight */
public int lineH;
private boolean lineHset;
/** Default line height. */
public void setLineHeight(int k)
{
this.lineH = k;
lineHset = true;
}
public int getLineHeight() {return lineH;}
/** Constructs a new Tree based on an empty TreeModel. */
public Tree()
{
this(new TreeModel());
}
/** Constructs a new Tree based on the given parameters.
* @param model the TreeModel to be used
*/
public Tree(TreeModel model)
{
this(model, true);
}
/** Constructs a new Tree based on the given parameters.
* @param model the TreeModel to be used
* @param showRoot if true, the root node is shown
*/
public Tree(TreeModel model, boolean showRoot)
{
super.add(vbar = Settings.fingerTouch ? new ScrollPosition(ScrollBar.VERTICAL) : new ScrollBar(ScrollBar.VERTICAL));
super.add(hbar = Settings.fingerTouch ? new ScrollPosition(ScrollBar.HORIZONTAL) : new ScrollBar(ScrollBar.HORIZONTAL));
vbar.addPressListener(this);
hbar.addPressListener(this);
addPenListener(this);
addKeyListener(this);
vbar.setLiveScrolling(true);
hbar.setLiveScrolling(true);
vbar.setFocusLess(true);
hbar.setFocusLess(true);
this.showRoot = showRoot;
this.allowsChildren = model == null || model.allowsChildren;
setModel(model);
focusTraversable = true;
ignoreOnAddAgain = ignoreOnRemove = true;
setCursorColor(0xE1FFFF); // aqua (cursor color) highlight
if (Settings.fingerTouch)
flick = new Flick(this);
}
public boolean flickStarted()
{
isFlicking = true;
return isScrolling;
}
public void flickEnded(boolean atPenDown)
{
isFlicking = false;
flickDirection = NONE;
}
public boolean canScrollContent(int direction, Object target)
{
if (flickDirection == NONE)
flickDirection = direction == DragEvent.UP || direction == DragEvent.DOWN ? VERTICAL : HORIZONTAL;
if (Settings.fingerTouch)
switch (direction)
{
case DragEvent.UP : return vbar.getValue() > vbar.getMinimum();
case DragEvent.DOWN : return (vbar.getValue() + vbar.getVisibleItems()) < vbar.getMaximum();
case DragEvent.LEFT : return hbar.getValue() > hbar.getMinimum();
case DragEvent.RIGHT: return (hbar.getValue() + hbar.getVisibleItems()) < hbar.getMaximum();
}
flickDirection = NONE;
return false;
}
private int hbarX0,vbarY0,hbarDX,vbarDY;
private boolean scScrolled;
private static final int NONE = 0;
private static final int VERTICAL = 1;
private static final int HORIZONTAL = 2;
private int flickDirection = NONE;
private boolean isFlicking;
public boolean scrollContent(int dx, int dy, boolean fromFlick)
{
boolean scrolled = false;
if (flickDirection == HORIZONTAL && dx != 0)
{
hbarDX += dx;
int oldValue = hbar.getValue();
hbar.setValue(hbarX0 + hbarDX);
lastH = hbar.getValue();
if (oldValue != lastH)
{
hsOffset = lastH;
scrolled = true;
if (!fromFlick) hbar.tempShow();
}
}
if (flickDirection == VERTICAL && dy != 0)
{
vbarDY += dy;
int oldValue = vbar.getValue();
vbar.setValue(vbarY0 + vbarDY / lineH);
lastV = vbar.getValue();
if (oldValue != lastV)
{
offset = lastV;
scrolled = true;
if (!fromFlick) vbar.tempShow();
}
}
if (scrolled)
{
Window.needsPaint = true;
return true;
}
else
return false;
}
public int getScrollPosition(int direction)
{
if (direction == DragEvent.LEFT || direction == DragEvent.RIGHT)
return hsOffset;
return offset;
}
public Flick getFlick()
{
return flick;
}
public boolean wasScrolled()
{
return scScrolled;
}
/** Call this method to hide the file and folder icons. */
public void dontShowFileAndFolderIcons()
{
this.showIcons = false;
this.imgOpenH = this.imgOpenW = 0;
}
/**
* Method to set the tree model.
*
* @param model
* the tree model.
*/
public void setModel(TreeModel model)
{
clear();
this.model = (model != null) ? model : new TreeModel();
model.setTree(this);
initTree(model.getRoot());
}
/**
* Method to set the scrollbar appearance.
* @see #SCROLLBAR_ALWAYS
* @see #SCROLLBAR_AS_NEEDED
* @see #SCROLLBAR_NEVER
*/
public void setScrollBarPolicy(int horiz, int vert)
{
hbarPolicy = horiz;
switch (horiz)
{
case SCROLLBAR_ALWAYS:
{
if (!hbar.isVisible()) resetScrollBars();
break;
}
case SCROLLBAR_NEVER:
{
hbar.setVisible(false);
break;
}
default:
case SCROLLBAR_AS_NEEDED:
{
resetScrollBars();
break;
}
}
}
/**
* Method to set the tree root node with the new root node. If the new root node is null, the tree is unchanged.
*
* @param root
* the new tree root node.
*/
public void initTree(Node root)
{
if (root != null)
{
if (showRoot)
{
items.addElement(root);
root.expanded = false;
root.level = 0;
expand(root);
}
else
{
for (int i = 0, n = root.size(); i < n; i++)
{
Node node = (Node)root.items[i];
items.addElement(node);
node.level = 1;
node.expanded = false;
}
}
}
resetScrollBars();
}
public void onFontChanged()
{
if (!lineHset)
lineH = Settings.fingerTouch ? fmH*3/2 : fmH;
}
/**
* Method to initialize the vertical and horizontal scrollbars maximum.
*/
protected void initScrollBars()
{
// initialize the vertical scrollbar
itemCount = items.size();
vbar.setEnabled(isEnabled() && visibleItems < itemCount);
vbar.setMaximum(itemCount);
// initialize the horizontal scrollbar
int maxWidth = 0;
for (int i = 0,n=items.size(); i < n; i++)
maxWidth = Math.max(getItemWidth(i), maxWidth);
maxWidth = maxWidth - (width - vbar.getPreferredWidth());
hsCount = (maxWidth > 0) ? maxWidth : 0;
hbar.setEnabled(isEnabled() && hsCount > width - vbar.getPreferredWidth());
hbar.setMaximum(hsCount);
switch (hbarPolicy)
{
case SCROLLBAR_ALWAYS:
hbar.setVisible(true);
break;
case SCROLLBAR_AS_NEEDED:
hbar.setVisible(hsCount > width - vbar.getPreferredWidth());
break;
case 2:
hbar.setVisible(false);
break;
}
}
/**
* Method to load icons use for the tree. You can change the icon by using the setIcon(int iconType, Filename
* imageFilename).
*/
protected void initImage()
{
try
{
if (imgOpenDefault == null)
{
imgCloseDefault = new Image("totalcross/res/closed_folder.png");
imgOpenDefault = new Image("totalcross/res/open_folder.png");
imgFileDefault = new Image("totalcross/res/document.png");
}
setIcon(ICON_PLUS, getIcon(true));
setIcon(ICON_MINUS, getIcon(false));
setIcon(ICON_CLOSE, imgCloseDefault);
setIcon(ICON_OPEN, imgOpenDefault);
setIcon(ICON_FILE, imgFileDefault);
}
catch (Exception e)
{
// Should never happen
}
x0 = Math.max(imgOpenW,imgPlusSize) + (Settings.isWindowsDevice() ? 4 : 3);
}
/**
* plusIcon dynamically creates a "+" icon, based on current size of the font.
*
* @return icon of a boxed plus icon
* @throws ImageException
*/
private Image getIcon(boolean plus) throws ImageException
{
int w;
int mid;
Image img;
Graphics gImg;
w = fmH/2;
if ((w % 2) == 0) w++; // make sure we have an odd number of pixels for our plus sign
img = new Image(w, w);
gImg = img.getGraphics();
gImg.backColor = Color.WHITE;
gImg.foreColor = Color.BLACK;
gImg.drawRect(0,0,w,w);
gImg.fillRect(1, 1, w - 2, w - 2);
mid = (w / 2); // where is the midpoint of our +
if (plus)
gImg.drawLine(mid, 2, mid, w - 3); // vertical slash
gImg.drawLine(2, mid, w - 3, mid); // draw horizontal slash
return img;
}
/**
* Method to set the icon of the tree based on the icon type. Note: You should not change the plus and minus icons.
* You can set the open/close to null (setting one will null out the other).
*
* @param iconType
* one of the ICON_xxx constants.
* @param img
* The image to be used.
* @throws ImageException
* @see #ICON_PLUS
* @see #ICON_MINUS
* @see #ICON_OPEN
* @see #ICON_CLOSE
* @see #ICON_FILE
*/
public void setIcon(int iconType, Image img) throws ImageException
{
if (iconType > 1)
{
img = img.smoothScaledFixedAspectRatio(lineH > fmH ? fmH : lineH*8/10,true); // guich@tc110_19
if (iconType == ICON_OPEN || iconType == ICON_CLOSE)
img.applyColor2(backColor);
}
switch (iconType)
{
case ICON_PLUS:
imgPlus = getIcon(true);
imgPlusSize = imgPlus.getWidth();
break;
case ICON_MINUS:
imgMinus = getIcon(false);
break;
case ICON_CLOSE:
imgClose = img;
break;
case ICON_OPEN:
imgOpen = img;
imgOpenW = imgOpen.getWidth();
imgOpenH = imgOpen.getHeight();
break;
case ICON_FILE:
imgFile = img;
break;
}
}
/**
* Method to return the width of the given item index with the current fontmetrics. Note: if you override this class
* you must implement this method.
*/
protected int getItemWidth(int index)
{
return fm.stringWidth(items.items[index].toString());
}
/**
* Method to empties this Tree, setting all elements of the array to null, so they can be garbage collected.
*/
public void removeAll()
{
model = new TreeModel();
clear();
}
/**
* Same as removeAll() method. Just more clearer method name
*/
public void clear()
{
items.removeAllElements();
vbar.setMaximum(0);
hbar.setMaximum(0);
itemCount = hsCount = offset = hsOffset = 0;
selectedIndex = -1;
Window.needsPaint = true;
}
/**
* Method to insert the items to the tree (For internal uses) Note: this method does not reset the scroll bar, you
* need to call this resetScrollBars() after you have performed an insert.
*/
private void insert(int index, Node node, int level, int expandValue)
{
items.insertElementAt(node, index);
node.level = level;
node.expanded = expandValue == 1;
}
/**
* Method to remove the given index from the Tree items vector. This method will not remove the node from the
* original node.
*
* @param index
* the item index in the items vector.
*/
public void remove(int index)
{
if (index < 0 || index > itemCount - 1) return;
int level = ((Node)items.items[index]).level;
do
{
items.removeElementAt(index);
itemCount--;
} while (index < itemCount && level < ((Node)items.items[index]).level); // guich@tc126_42: inverted test
resetScrollBars();
Window.needsPaint = true;
}
/**
* Method to remove an Object from the Tree's items vector.
*
* @param item the Node to delete from the tree's item vector.
*/
public void remove(Object item)
{
int index = items.indexOf(item);
if (itemCount > 0 && index != -1) remove(index);
}
/**
* Method to reset the horizontal scroll bar properties.
*/
private void resetHBar()
{
if (hbarPolicy == SCROLLBAR_NEVER) return;
// calculate the horizontalscrollbar maximum
int max = 0;
int indent = 3 + (imgPlusSize + hline + imgOpenW / 2 - imgPlusSize / 2);
for (int i = 0,n=items.size(); i < n; i++)
{
Node nn = (Node) items.items[i];
max = Math.max(max, fm.stringWidth(nn.getNodeName()) + indent * nn.level);
}
max += vbar.getPreferredWidth(); // remember to take into account of the pixels used to draw the icons and scrollbar
hbar.setMaximum(max);
if (hbarPolicy == SCROLLBAR_ALWAYS || (width - vbar.getPreferredWidth()) < max)
{
hbar.setEnabled(isEnabled() && (width - vbar.getPreferredWidth()) < max);
hbar.setVisible(true);
}
else
hbar.setVisible(false);
}
/**
* Method to reset the horizontal scroll bar properties.
*/
private void resetVBar()
{
itemCount = items.size();
vbar.setMaximum(itemCount);
boolean wasDisabled = !vbar.isEnabled();
vbar.setEnabled(isEnabled() && visibleItems < itemCount);
if (vbar.isEnabled() && wasDisabled) // guich@tc126_4: reset vbar position if items got above visible items
vbar.setValue(0);
if (selectedIndex == itemCount) // last item was removed?
setSelectedIndex(selectedIndex - 1);
if (itemCount == 0)
selectedIndex = -1;
if (itemCount <= visibleItems && offset != 0)
offset = 0;
}
/**
* Method to rest the vertical and horizontal scrollbars properties. Note: there's still a bug in resetting the
* horizontal scroll bar.
*/
private void resetScrollBars()
{
resetVBar();
resetHBar();
}
/**
* Method to expand a collapsed node.
*
* @param node
* the collapse node to expand.
* @return True if the item was expanded, false otherwise.
*/
public boolean expand(Node node)
{
int index,n;
if (!node.expanded && (index=indexOf(node)) != -1 && !node.isLeaf(allowsChildren) && (n=node.size()) > 0) // guich@tc126_5: added last test to prevent that a root-only tree sets expand to true
{
node.expanded = true;
int level = node.level+1; // our children have one less our level
for (int i = 0; i < n; i++)
{
index++;
insert(index, (Node)node.items[i], level, 0);
}
resetScrollBars();
Window.needsPaint = true;
return true;
}
return false;
}
/**
* Method to collapse an expanded node.
*
* @param node
* the expanded node to collapse.
* @return True if the item was collapsed, false otherwise.
*
*/
public boolean collapse(Node node)
{
int index;
if (node.expanded && (index=indexOf(node)) != -1 && !node.isLeaf(allowsChildren))
{
int level = node.level;
index++;
while (index < itemCount && level < ((Node)items.items[index]).level)
{
items.removeElementAt(index);
itemCount--;
}
((Node)items.items[index - 1]).expanded = false;
resetScrollBars();
Window.needsPaint = true;
return true;
}
return false;
}
/**
* Method to set the Object at the given Index, starting from 0.
*
* @param i
* the index
* @param s
* the object to set.
*/
public void setItemAt(int i, Object s)
{
if (0 <= i && i < itemCount)
{
items.items[i] = s;
Window.needsPaint = true;
}
}
/**
* Method to get the Object at the given Index. Returns an empty string in case of error.
*
* @param i
* the index.
*/
public Object getItemAt(int i)
{
if (0 <= i && i < itemCount) return items.items[i];// get(i);
return "";
}
/**
* Method to return the selected item of the Tree or an empty String if no selection has been made.
*
* @return the selected object, or null is no selection has been made.
*/
public Node getSelectedItem()
{
return selectedIndex >= 0 ? (Node) items.items[selectedIndex] : null;
}
/**
* Method to return the position of the selected item of the Tree or -1 if the Tree has no selected index yet.
*
* @return the selected index or -1 if no selection has been made.
*/
public int getSelectedIndex()
{
return selectedIndex;
}
/**
* Method to return all items in the items vector as an array of object. The objects are of the class Node.
*
* @return all items in items vector as an array of Objects.
*/
public Object[] getItems()
{
return items.toObjectArray();
}
/**
* Method to return the index of the item specified by the name, or -1 if not found.
*
* @param name
* the object to find.
* @return the index of the item specified by the name, or -1 if not found.
*/
public int indexOf(Object name)
{
return items.indexOf(name);
}
/**
* Method to select the given name. If the name is not found, the current selected item is not changed.
*
* @since SuperWaba 4.01
* @param name
* the object to select.
*/
public void setSelectedItem(Object name)
{
int pos = indexOf(name);
if (pos != -1) setSelectedIndex(pos);
}
/**
* Method to select the given index and scroll to it if necessary. Note: select must be called only after the control
* has been added to the container and its rect has been set.
*
* @param i
* the index of the item.
*/
public void setSelectedIndex(int i)
{
if (0 <= i && i < itemCount && i != selectedIndex && height != 0)
{
int vi = vbar.getVisibleItems();
if (i < offset || i >= offset+vi) // guich@tc125_20: change offset only if the item is not visible
{
offset = i-vi/2; // guich@tc125_20: make it centered on screen
if (offset < 0)
offset = 0;
int ma = vbar.getMaximum();
if (offset + vi > ma)
offset = Math.max(ma - vi, 0);
}
selectedIndex = i;
vbar.setValue(offset);
Window.needsPaint = true;
}
else
if (i == -1)
{
offset = 0;
vbar.setValue(0);
selectedIndex = -1;
Window.needsPaint = true;
}
}
/**
* Returns the number of items (Nodes)
*/
public int size()
{
return itemCount;
}
/**
* Do nothing.
*/
public void add(Control control)
{
}
/**
* Do nothing.
*/
public void remove(Control control)
{
}
/**
* Method to return the preferred width, ie, size of the largest item plus 20.
*
* @return the preferred width of this control.
*/
public int getPreferredWidth()
{
int maxWidth = 0;
int n = itemCount;
for (int i = 0; i < n; i++)
maxWidth = Math.max(getItemWidth(i), maxWidth);
return maxWidth + 6 + vbar.getPreferredWidth() + insets.left+insets.right;
}
/**
* Method to return the number of items multiplied by the font metrics height
*
* @return the preferred height of this control.
*/
public int getPreferredHeight()
{
int n = itemCount;
int h = Math.max(lineH * n, vbar.getPreferredHeight()) + 6;
return (n == 1 ? h - 1 : h) + insets.top+insets.bottom;
}
/**
* Method to search this Tree for an item with the first letter matching the given char. The search is made case
* insensitive. Note: if you override this class you must implement this method.
*/
protected void find(char c)
{
for (int i = 0; i < itemCount; i++)
{
String s = items.items[i].toString();
// first letter matches and not the already selected index?
if (s.length() > 0 && Convert.toUpperCase(s.charAt(0)) == c && selectedIndex != i)
{
setSelectedIndex(i);
Window.needsPaint = true;
break; // end the for loop
}
}
}
/**
* Method to enable this control if the specified enabled flag is true.
*/
public void setEnabled(boolean enabled)
{
if (internalSetEnabled(enabled,false))
{
vbar.setEnabled(isEnabled() && visibleItems < itemCount);
hbar.setEnabled(isEnabled());
}
}
public void setBackColor(int c)
{
super.setBackColor(c);
if (imgPlus == null)
initImage();
}
protected void onColorsChanged(boolean colorsChanged)
{
if (colorsChanged)
{
vbar.setBackForeColors(backColor, foreColor);
hbar.setBackForeColors(backColor, foreColor);
}
fColor = getForeColor();
bgColor0 = Color.brighter(getBackColor());
bgColor1 = cursorColor != -1 ? cursorColor : (bgColor0 != Color.WHITE) ? backColor : Color.getCursorColor(bgColor0);
if (fColor == bgColor1)
fColor = foreColor;
Graphics.compute3dColors(isEnabled(), backColor, foreColor, fourColors);
}
/**
* Method to recalculate the box size for the selected item if the control is resized by the main application .
*/
protected void onBoundsChanged(boolean screenChanged)
{
onFontChanged();
int btnW = vbar.getPreferredWidth();
int btnH = hbar.getPreferredHeight();
if (Settings.fingerTouch && ScrollPosition.AUTO_HIDE)
btnW = btnH = 0;
visibleItems = ((height - 2 - btnH) / lineH);
vbar.setMaximum(itemCount);
vbar.setVisibleItems(visibleItems);
vbar.setEnabled(visibleItems < itemCount);
btnX = width - btnW;
vbar.setRect(Settings.fingerTouch ? RIGHT-2 : RIGHT, 0, PREFERRED, FILL, null, screenChanged);
hbar.setRect(0, Settings.fingerTouch ? BOTTOM-2 : BOTTOM, btnW == 0 ? FILL : FIT, PREFERRED, null, screenChanged);
resetScrollBars();
Window.needsPaint = true;
}
/**
* Method to notify the tree that a node has been removed from the tree model and to repaint the tree to reflect the
* changes, if necessary
*
* @param node
* the node that has been removed from the tree model
*/
public void nodeRemoved(Node node)
{
remove(indexOf(node));
}
/**
* Method to notify the tree that a node has been added to the tree model
* and to repaint the tree to reflect the changes.
*
* @param parent
* the parent node of the new added node
* @param child
* the new ly added node
* @param index
* the index of the new node
*/
public void nodeInserted(Node parent, Node child, int index)
{
int pos = indexOf(parent);
if (pos < 0) return; // didn't find parent node
if (!parent.expanded) return; // node is not expanded, so we don't have to paint the node
int lvl = parent.level + 1;
int count = 0;
for (int i = pos+1,n=items.size(); i < n; i++)
{
int l = ((Node)items.items[i]).level;
if (lvl == l)
{
if (count == index)
{
insert(i, child, lvl, 0);
break;
}
count++;
}
// else - guich@tc120_14: no else here!
if (lvl > l || i == n - 1)
{
insert(lvl > l ? i : i+1, child, lvl, 0);
break;
}
}
resetScrollBars();
Window.needsPaint = true;
}
/**
* Method to notify the tree that a node in the tree model has been modified (currently - only changing the user
* object)
*
* @param node
* the node that has been modified
*/
public void nodeModified(Node node)
{
if (indexOf(node) != -1)
{
resetScrollBars();
Window.needsPaint = true;
}
}
public void penDrag(DragEvent de)
{
if (Settings.fingerTouch)
{
int dx = -de.xDelta;
int dy = -de.yDelta;
if (isScrolling)
{
scrollContent(dx, dy, true);
de.consumed = true;
}
else
{
int direction = DragEvent.getInverseDirection(de.direction);
//de.consumed = true; - with this, the ScrollPositions don't appear
if (canScrollContent(direction, de.target) && scrollContent(dx, dy, true))
scScrolled = isScrolling = true;
}
de.consumed = true; // guich@tc166: if inside a TabbedContainer, prevent it from scrolling
}
}
public void penDragEnd(DragEvent e) {}
public void penDragStart(DragEvent e) {}
public void penDown(PenEvent pe)
{
if (pe.target != this) return;
scScrolled = false;
vbarY0 = vbar.getValue();
hbarX0 = hbar.getValue();
hbarDX = vbarDY = 0;
if (!(pe.target instanceof ScrollBar || Settings.fingerTouch)) // the click is inside the scrollbar, get out
computeSel(pe);
}
private void computeSel(PenEvent pe)
{
lastControl = null;
int sel = ((pe.y - 4) / lineH) + offset;
if (sel < itemCount)
{
if (multipleSelection && pe.x < imgPlusSize+2)
checkClicked(sel);
else
if (pe.x < btnX)
{
Node node = (Node)items.items[sel];
if (sel != selectedIndex)
{
selectedIndex = sel;
Window.needsPaint = true;
}
if (node.userObject != null && node.userObject instanceof Control)
{
postControlEvent(node, pe);
Window.needsPaint = true;
}
}
}
}
public void penUp(PenEvent pe)
{
if (pe.target != this) return;
if (!isFlicking)
flickDirection = NONE;
isScrolling = false;
if (pe.target instanceof ScrollBar) // the click is inside the scrollbar, get out
return;
if (!scScrolled/* && Settings.fingerTouch*/) // guich@tc310: had to comment fingerTouch to allow userObject's Control receive the PEN_UP even
computeSel(pe);
// Post the event
int sel = ((pe.y - 4) / lineH) + offset;
if (isInsideOrNear(pe.x,pe.y) && pe.x < btnX && sel < itemCount)
{
postPressedEvent();
if (multipleSelection && pe.x < imgPlusSize+2) // don't expand/collapse when pressing the CheckBox
return;
Node node = (Node) items.items[sel];
int xstart = getTextX(node.level);
boolean isLeaf = node.isLeaf(allowsChildren);
if (isLeaf && pe.x >= xstart)
node.visited = true;
else
if (((!scScrolled || !Settings.fingerTouch) && expandClickingOnText) || (!scScrolled && pe.x < xstart))
{
// call expand and collapse or change the leaf icon on when clicked anywhere
if (!node.expanded)
expand(node);
else
collapse(node);
}
Window.needsPaint = true;
}
}
public void checkClicked(int sel)
{
Node n = ((Node)items.items[sel]);
n.isChecked = !n.isChecked;
Window.needsPaint = true;
}
public void controlPressed(ControlEvent e)
{
if (e.target == vbar)
{
int newOffset = vbar.getValue();
if (newOffset != offset)
{
offset = newOffset;
Window.needsPaint = true;
}
}
else
if (e.target == hbar)
{
int hsValue = hbar.getValue();
if (hsValue != hsOffset && hsValue > -1)
{
hsOffset = hsValue;
Window.needsPaint = true;
}
}
}
public void actionkeyPressed(KeyEvent e)
{
postPressedEvent(); // guich@tc111_21
}
public void keyPressed(KeyEvent e)
{
if (lastControl != null)
{
e.target = lastControl;
lastControl.onEvent(e);
}
else
if (multipleSelection && selectedIndex >= 0 && ((KeyEvent)e).key == ' ')
checkClicked(selectedIndex);
else
find(Convert.toUpperCase((char) ((KeyEvent)e).key));
}
public void specialkeyPressed(KeyEvent e)
{
if (lastControl != null)
{
e.target = lastControl;
lastControl.onEvent(e);
}
else
handleKeys((KeyEvent)e);
}
private boolean handleKeys(KeyEvent ke)
{
if (ke.modifiers != 0 && (ke.isDownKey() || ke.isUpKey())) // with shift/control/alt, pass to the scrollbars
vbar.onEvent(ke);
else
if (ke.modifiers != 0 && (ke.key == SpecialKeys.LEFT || ke.key == SpecialKeys.RIGHT))
hbar.onEvent(ke);
else
if (ke.isDownKey() && selectedIndex < itemCount-1)
setSelectedIndex(selectedIndex+1);
else
if (ke.isUpKey() && selectedIndex > 0)
setSelectedIndex(selectedIndex-1);
else
if (selectedIndex >= 0 && ke.key == SpecialKeys.LEFT)
return collapse((Node)items.items[selectedIndex]);
else
if (selectedIndex >= 0 && ke.key == SpecialKeys.RIGHT)
return expand((Node)items.items[selectedIndex]);
else
if (ke.isActionKey() || ke.type == KeyEvent.ACTION_KEY_PRESS)
postPressedEvent();
else
if (multipleSelection && ke.key == ' ')
checkClicked(selectedIndex);
else
return false;
return true;
}
public Control handleGeographicalFocusChangeKeys(KeyEvent ke) // kmeehl@tc100
{
return handleKeys(ke) ? this : null;
}
public void onPaint(Graphics g)
{
if (imgPlus == null)
initImage();
// Draw background and borders
g.backColor = bgColor0;
g.fillRect(0, 0, btnX, height);
g.foreColor = foreColor;
g.draw3dRect(0, 0, width, height, Graphics.R3D_CHECK, false, false, fourColors);
// draw scrollbar border (why is it disappear in the first place? or is there a border for the scrollbar class??)
g.drawRect(btnX - 1, 0, vbar.getPreferredWidth() + 1, height);
if (!Settings.fingerTouch && hbar.isVisible()) g.drawRect(0, height - hbar.getPreferredHeight() - 1, width - vbar.getPreferredWidth() + 1, hbar.getPreferredHeight());
int dx = multipleSelection ? x0+imgPlusSize+2 : x0;
int dy = 2;
g.foreColor = fColor;
g.setClip(2, 1, btnX - 4, lineH * visibleItems + 1);
int greatestVisibleItemIndex = Math.min(itemCount, visibleItems + offset); // code corrected by Bjoem Knafla
for (int i = offset; i < greatestVisibleItemIndex; ++i, dy += lineH)
{
if (i == selectedIndex)
drawCursor(g, selectedIndex);
drawNode(g, i, dx - hsOffset, dy);
}
}
/**
* Method to draw the icons and node text
*/
protected void drawNode(Graphics g, int index, int dx, int dy)
{
Node node = (Node) items.items[index];
int level = node.level - 1;
dx += 2;
dy += 2;
if (index > 0) drawConnector(g, index, dx, dy, node); // draw the line that connect the nodes
boolean expand = node.expanded;
int x = dx + (imgPlusSize + hline + gap + imgOpenW / 2 - imgPlusSize / 2) * level;
int y = dy;
boolean nodeIsLeaf = node.isLeaf(allowsChildren);
// draw plus minus icon
y = dy + lineH / 2;
if (nodeIsLeaf)
g.drawDots(x + imgPlusSize / 2, y, x + imgPlusSize, y);
else
if (node.size() == 0)
g.drawDots(x + imgPlusSize / 2, y, x + imgPlusSize, y);
else
g.drawImage(expand ? imgMinus : imgPlus, x, y - imgPlusSize / 2);
// draw horizontal line
x += imgPlusSize;
g.drawDots(x, y, x + hline, y);
if (multipleSelection)
{
g.foreColor = fColor; // restore text color
g.backColor = backColor;
int k = imgPlusSize+1;
y = (lineH-k)/2 + dy+1;
int rx = 3 - hsOffset;
if (node.isChecked)
{
if (uiVista || uiAndroid)
g.fillVistaRect(rx,y,k,k,backColor,false,false);
else
g.fillRect(rx,y,k,k);
}
g.drawRect(rx,y,k,k); // guich@220_28
g.foreColor = foreColor; // restore text color
}
// draw folder icon (remember the gap needed)
x += hline + gap;
y = dy + lineH / 2 - imgOpenH / 2;
if (showIcons)
{
if (nodeIsLeaf)
drawFileImage(g, node, x, y);
else
g.drawImage(expand ? imgOpen : imgClose, x, y);
}
if (node.userObject != null && node.userObject instanceof Control)
{
x += imgOpenW + gap+1;
y = dy;
Control c = (Control)node.userObject;
int ww = width-x - (!uiAndroid ? vbar.getWidth() : 0) - 2 - hsOffset;
int hh = lineH-2;
if (c.getWidth() == 0)
{
super.add(c); // caution: this.add does nothing!
c.setRect(0,0,ww,hh);
}
else if (c.getWidth() != ww) // allow rotation
{
c.intXYWH(0,0,ww,hh);
c.reposition();
}
c.intXYWH(x,y,ww,hh);
c.onPaint(c.getGraphics());
if (c instanceof Container)
((Container)c).paintChildren();
c.intXYWH(100000,0,ww,hh);
}
else
{
dy--;
x += imgOpenW + gap + gap + 2;
y = dy+(lineH-fmH)/2;
String text = node.toString();
if (node.backColor != -1) // guich@tc120_13
{
g.backColor = node.backColor;
g.fillRect(x,y,fm.stringWidth(text),lineH);
}
if (node.foreColor != -1) // guich@tc120_13
g.foreColor = node.foreColor;
g.drawText(text, x, y, textShadowColor != -1, textShadowColor);
}
}
private PenEvent pec = new PenEvent();
private Control lastControl;
private void postControlEvent(Node node, PenEvent peorig)
{
Control c = (Control)node.userObject;
pec.type = peorig.type;
pec.touch();
int xstart = getTextX(node.level);
int sel = ((peorig.y - 4) / lineH) + offset;
pec.x = peorig.x - xstart + c.getX() - gap;
pec.y = peorig.y - sel * lineH - 4;
if (c instanceof Container)
{
c = ((Container) c).findChild(pec.x -= 100000,pec.y);
pec.x -= c.getX();
pec.y -= c.getY();
}
pec.target = lastControl = c;
c.postEvent(pec);
}
private int getTextX(int level) // guich@tc125_24
{
int dx = multipleSelection ? x0+imgPlusSize+2 : x0;
dx -= hsOffset;
level--;
int x = dx + (imgPlusSize + hline + gap + imgOpenW / 2 - imgPlusSize / 2) * level;
x += imgPlusSize;
x += hline + gap;
x += imgOpenW + gap + gap + 2;
return x-1;
}
/** Allows the draw of customized file images. */
protected void drawFileImage(Graphics g, Node node, int x, int y)
{
if (imgFile != null)
g.drawImage(imgFile, x, y);
}
/**
* Method to draw the (line connector) angled line.
*/
protected void drawConnector(Graphics g, int index, int dx, int dy, Node node)
{
if (node == null) return;
int level = node.level - 1;
Node prev=null,next=null;
Node parent = node.parent;
if (parent != null)
{
int pos = parent.indexOf(node);
if (pos > 0)
prev = (Node)parent.items[pos - 1];
if (pos >= 0 && pos < parent.size()-1)
next = (Node)parent.items[pos + 1];
}
// calculate the x-start position
int x = dx;
if (level == 0)
x += imgPlusSize / 2;
else
if (level == 1)
x += imgPlusSize + hline + gap + imgOpenW / 2;
else
x += imgPlusSize + hline + gap + imgOpenW / 2 + (imgPlusSize / 2 + hline + gap + imgOpenW / 2 + 1) * (level - 1);
// calculate the y-start and y-end position
int ystart;
int yend;
// handles the last level 1 node
if (level == 0 && next == null && prev != null && items.items[index] == node)
{
ystart = dy - (lineH - imgPlusSize) / 2;
yend = dy + (lineH - imgPlusSize) / 2;
g.drawDots(x, ystart, x, yend);
}
// draw vertical connector lines for leaf node
if (node.isLeaf(allowsChildren) || node.size() == 0)
{
ystart = dy - (lineH - imgOpenH) / 2;
yend = dy + (lineH / 2);
g.drawDots(x, ystart, x, yend);
if (next != null)
{
ystart = yend;
yend += (lineH / 2);
g.drawDots(x, ystart, x, yend);
}
}
// draw vertical connector lines for folder node
else
{
if (next == null && node == items.items[index])
{
ystart = dy - (lineH - imgPlusSize) / 2;
yend = dy + (lineH - imgPlusSize) / 2;
g.drawDots(x, ystart, x, yend); // draw from "+" to end of line
}
if (next != null)
{
ystart = dy - (lineH - imgPlusSize) / 2;
yend = dy + lineH;
g.drawDots(x, ystart, x, yend);
}
}
drawConnector(g, index, dx, dy, node.parent);
}
/**
* Method to draw the highlight box when user select a listbox's item.
*/
protected void drawCursor(Graphics g, int sel)
{
if (offset <= sel && sel < visibleItems + offset && sel < itemCount)
{
Node n = (Node)items.items[sel];
int level = n.level;
int x0 = this.x0;
if (multipleSelection)
x0 += imgPlusSize+2;
int dx = x0 - hsOffset -1;
if (level == 1)
dx += (imgPlusSize + hline + gap + imgOpenW) * level;
else
dx += imgPlusSize + hline + gap + imgOpenW + (imgPlusSize + hline + gap + imgOpenW / 2 - imgPlusSize / 2) * (level - 1);
dx += 3;
int dy = 4;
dy += (sel - offset) * lineH;
g.setClip(useFullWidthOnSelection ? 2 : dx - 1, dy - 1, btnX - (useFullWidthOnSelection ? 2 : dx), Math.min(lineH * visibleItems, this.height - dy));
int oldb = g.backColor;
g.backColor = bgColor1;
int extraH = n.userObject != null && n.userObject instanceof Control ? 0 : fm.descent - 1;
if (useFullWidthOnSelection)
g.fillRect(2,dy-1,btnX-2,lineH + extraH);
else
g.fillRect(dx + 1, dy-1, this.width-dx, lineH + extraH);
g.clearClip();
g.backColor = oldb;
}
}
/**
* Method to set the cursor color for this Tree. The default is equal to the background slightly darker.
*/
public void setCursorColor(int color)
{
this.cursorColor = color;
onColorsChanged(true);
}
/**
* Method to reload the tree. Use this method when the tree model has made a drastic change.
*/
public void reload()
{
clear();
initTree(model.getRoot());
Window.needsPaint = true;
}
/**
* Method to clear the tree and release the tree model references.
*/
public void unload()
{
clear();
model = null;
}
public void getFocusableControls(Vector v)
{
if (visible && isEnabled()) v.addElement(this);
}
}