blob: 57d508fda45a6837d89c801fd98b4d760cd4cccf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk.skin.terra;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.GeneralPath;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.collections.Sequence.Tree.Path;
import org.apache.pivot.util.ClassUtils;
import org.apache.pivot.util.Filter;
import org.apache.pivot.util.Utils;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.Button;
import org.apache.pivot.wtk.Checkbox;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.GraphicsUtilities;
import org.apache.pivot.wtk.Keyboard;
import org.apache.pivot.wtk.Keyboard.KeyCode;
import org.apache.pivot.wtk.Keyboard.KeyLocation;
import org.apache.pivot.wtk.Keyboard.Modifier;
import org.apache.pivot.wtk.Mouse;
import org.apache.pivot.wtk.Orientation;
import org.apache.pivot.wtk.Platform;
import org.apache.pivot.wtk.Theme;
import org.apache.pivot.wtk.TreeView;
import org.apache.pivot.wtk.TreeView.SelectMode;
import org.apache.pivot.wtk.TreeViewBranchListener;
import org.apache.pivot.wtk.TreeViewListener;
import org.apache.pivot.wtk.TreeViewNodeListener;
import org.apache.pivot.wtk.TreeViewNodeStateListener;
import org.apache.pivot.wtk.TreeViewSelectionListener;
import org.apache.pivot.wtk.skin.ComponentSkin;
/**
* Tree view skin.
*/
public class TerraTreeViewSkin extends ComponentSkin implements TreeView.Skin, TreeViewListener,
TreeViewBranchListener, TreeViewNodeListener, TreeViewNodeStateListener,
TreeViewSelectionListener {
/**
* Node info visitor interface.
*/
protected interface NodeInfoVisitor {
/**
* Visits the specified node info.
*
* @param nodeInfo The object to visit
*/
public void visit(NodeInfo nodeInfo);
}
/**
* Iterates through the visible nodes. For callers who wish to know the path
* of each visible node, using this iterator will be much more efficient
* than manually iterating over the visible nodes and calling
* <tt>getPath()</tt> on each node.
*/
protected final class VisibleNodeIterator implements Iterator<NodeInfo> {
private int index;
private int end;
private Path path = null;
private NodeInfo previous = null;
public VisibleNodeIterator() {
this(0, visibleNodes.getLength() - 1);
}
/**
* Creates a new visible node iterator that will iterate over a portion
* of the visible nodes list (useful during painting).
*
* @param start The start index, inclusive
* @param end The end index, inclusive
*/
public VisibleNodeIterator(int start, int end) {
if (start < 0 || end >= visibleNodes.getLength()) {
throw new IndexOutOfBoundsException();
}
this.index = start;
this.end = end;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return (index <= end);
}
/**
* {@inheritDoc}
*/
@Override
public NodeInfo next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
NodeInfo next = visibleNodes.get(index++);
if (path == null) {
// First iteration
path = next.getPath();
} else if (next.parent == previous) {
// Child of previous visible node
path.add(0);
} else {
int n = path.getLength();
while (next.parent != previous.parent) {
path.remove(--n, 1);
previous = previous.parent;
}
int tail = path.get(n - 1);
path.update(n - 1, tail + 1);
}
previous = next;
return next;
}
/**
* This operation is not supported by this iterator.
*
* @throws UnsupportedOperationException always since this is unsupported.
*/
@Override
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Gets the index of the node last returned by a call to {@link #next()}
* , as seen in the current visible nodes list. Note that as branches
* are expanded and collapsed, the row index of any given node in the
* tree will change.
*
* @return The row index of the current node, or <tt>-1</tt> if
* <tt>next()</tt> has not yet been called.
*/
public int getRowIndex() {
return (path == null ? -1 : index - 1);
}
/**
* Gets the path of the node last returned by a call to {@link #next()}.
*
* @return The path to the node, or <tt>null</tt> if <tt>next()</tt> has
* not yet been called.
*/
public Path getPath() {
return path;
}
}
/**
* An internal data structure that keeps track of skin-related metadata for
* a tree node. The justification for the existence of this class lies in
* the <tt>visibleNodes</tt> data structure, which is a flat list of nodes
* that are visible at any given time. In this context, visible means that
* their parent hierarchy is expanded, <b>not</b> that they are being
* painted. This list, combined with <tt>getNodeHeight()</tt>, enables us to
* quickly determine which nodes to paint given a graphics clip rect. It
* also enables us to quickly traverse the tree view when handling key
* events. <p> NOTE: some of this data is managed by <tt>TreeView</tt> and
* cached here to provide further optimizations during painting and user
* input.
*/
protected static class NodeInfo {
// Core metadata
final TreeView treeView;
final BranchInfo parent;
final Object data;
final int depth;
// Cached fields. Note that this is maintained as a bitmask in favor of
// separate properties because it allows us to easily clear any cached
// field for all nodes in one common method. See #clearField(byte)
byte fields = 0;
public static final byte HIGHLIGHTED_MASK = 1 << 0;
public static final byte SELECTED_MASK = 1 << 1;
public static final byte DISABLED_MASK = 1 << 2;
public static final byte CHECKMARK_DISABLED_MASK = 1 << 3;
public static final byte CHECK_STATE_CHECKED_MASK = 1 << 4;
public static final byte CHECK_STATE_MIXED_MASK = 1 << 5;
public static final byte CHECK_STATE_MASK = CHECK_STATE_CHECKED_MASK
| CHECK_STATE_MIXED_MASK;
private NodeInfo(TreeView treeView, BranchInfo parent, Object data) {
this.treeView = treeView;
this.parent = parent;
this.data = data;
depth = (parent == null) ? 0 : parent.depth + 1;
// Newly created nodes are guaranteed to not be selected or checked,
// but they may be disabled or have their checkmarks disabled, so
// we set those flags appropriately here.
@SuppressWarnings("unchecked")
Filter<Object> disabledNodeFilter = (Filter<Object>) treeView.getDisabledNodeFilter();
if (disabledNodeFilter != null) {
setDisabled(disabledNodeFilter.include(data));
}
@SuppressWarnings("unchecked")
Filter<Object> disabledCheckmarkFilter = (Filter<Object>) treeView.getDisabledCheckmarkFilter();
if (disabledCheckmarkFilter != null) {
setCheckmarkDisabled(disabledCheckmarkFilter.include(data));
}
}
@SuppressWarnings("unchecked")
private static NodeInfo newInstance(TreeView treeView, BranchInfo parent, Object data) {
NodeInfo nodeInfo = null;
if (data instanceof List<?>) {
nodeInfo = new BranchInfo(treeView, parent, (List<Object>) data);
} else {
nodeInfo = new NodeInfo(treeView, parent, data);
}
return nodeInfo;
}
@SuppressWarnings("unchecked")
public Path getPath() {
Path path = Path.forDepth(depth);
NodeInfo nodeInfo = this;
while (nodeInfo.parent != null) {
List<Object> parentData = (List<Object>) nodeInfo.parent.data;
int index = parentData.indexOf(nodeInfo.data);
path.insert(index, 0);
nodeInfo = nodeInfo.parent;
}
return path;
}
public boolean isHighlighted() {
return ((fields & HIGHLIGHTED_MASK) != 0);
}
public void setHighlighted(boolean highlighted) {
if (highlighted) {
fields |= HIGHLIGHTED_MASK;
} else {
fields &= ~HIGHLIGHTED_MASK;
}
}
public boolean isSelected() {
return ((fields & SELECTED_MASK) != 0);
}
public void setSelected(boolean selected) {
if (selected) {
fields |= SELECTED_MASK;
} else {
fields &= ~SELECTED_MASK;
}
}
public boolean isDisabled() {
return ((fields & DISABLED_MASK) != 0);
}
public void setDisabled(boolean disabled) {
if (disabled) {
fields |= DISABLED_MASK;
} else {
fields &= ~DISABLED_MASK;
}
}
public boolean isCheckmarkDisabled() {
return ((fields & CHECKMARK_DISABLED_MASK) != 0);
}
public void setCheckmarkDisabled(boolean checkmarkDisabled) {
if (checkmarkDisabled) {
fields |= CHECKMARK_DISABLED_MASK;
} else {
fields &= ~CHECKMARK_DISABLED_MASK;
}
}
public TreeView.NodeCheckState getCheckState() {
TreeView.NodeCheckState checkState;
switch (fields & CHECK_STATE_MASK) {
case CHECK_STATE_CHECKED_MASK:
checkState = TreeView.NodeCheckState.CHECKED;
break;
case CHECK_STATE_MIXED_MASK:
checkState = TreeView.NodeCheckState.MIXED;
break;
default:
checkState = TreeView.NodeCheckState.UNCHECKED;
break;
}
return checkState;
}
public boolean isChecked() {
return ((fields & CHECK_STATE_CHECKED_MASK) != 0);
}
public void setCheckState(TreeView.NodeCheckState checkState) {
fields &= ~CHECK_STATE_MASK;
switch (checkState) {
case CHECKED:
fields |= CHECK_STATE_CHECKED_MASK;
break;
case MIXED:
fields |= CHECK_STATE_MIXED_MASK;
break;
case UNCHECKED:
break;
default:
break;
}
}
public void clearField(byte mask) {
fields &= ~mask;
}
@Override
public String toString() {
return ClassUtils.simpleToString(this);
}
}
/**
* An internal data structure that keeps track of skin-related metadata for
* a tree branch.
*/
protected static final class BranchInfo extends NodeInfo {
// Core skin metadata
private List<NodeInfo> children = null;
public static final byte EXPANDED_MASK = 1 << 6;
private BranchInfo(TreeView treeView, BranchInfo parent, List<Object> data) {
super(treeView, parent, data);
}
/**
* Loads this branch info's children. The children list is initialized
* to <tt>null</tt> and loaded lazily to allow the skin to only create
* <tt>NodeInfo</tt> objects for the nodes that it actually needs in
* order to paint. Thus, it is the responsibility of the skin to check
* if <tt>children</tt> is null and call <tt>loadChildren()</tt> if
* necessary.
*/
@SuppressWarnings("unchecked")
public void loadChildren() {
if (children == null || children.isEmpty()) {
List<Object> dataLocal = (List<Object>) this.data;
int count = dataLocal.getLength();
children = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
Object nodeData = dataLocal.get(i);
NodeInfo childNodeInfo = NodeInfo.newInstance(treeView, this, nodeData);
children.add(childNodeInfo);
}
}
}
public boolean isExpanded() {
return ((fields & EXPANDED_MASK) != 0);
}
public void setExpanded(boolean expanded) {
if (expanded) {
fields |= EXPANDED_MASK;
} else {
fields &= ~EXPANDED_MASK;
}
}
}
private BranchInfo rootBranchInfo = null;
private List<NodeInfo> visibleNodes = new ArrayList<>();
private NodeInfo highlightedNode = null;
private Path selectPath = null;
// Styles
private Font font;
private Color color;
private Color disabledColor;
private Color backgroundColor;
private Color selectionColor;
private Color selectionBackgroundColor;
private Color inactiveSelectionColor;
private Color inactiveSelectionBackgroundColor;
private Color highlightColor;
private Color highlightBackgroundColor;
private int spacing;
private int indent;
private boolean showHighlight;
private boolean showBranchControls;
private boolean showEmptyBranchControls;
private Color branchControlColor;
private Color branchControlSelectionColor;
private Color branchControlInactiveSelectionColor;
private Color gridColor;
private boolean showGridLines;
private boolean validateSelection = false;
private static final int BRANCH_CONTROL_IMAGE_WIDTH = 8;
private static final int BRANCH_CONTROL_IMAGE_HEIGHT = 8;
private static final int VERTICAL_SPACING = 1;
private static final Checkbox CHECKBOX = new Checkbox();
private static final int CHECKBOX_VERTICAL_PADDING = 2;
static {
CHECKBOX.setSize(CHECKBOX.getPreferredSize());
CHECKBOX.setTriState(true);
}
public TerraTreeViewSkin() {
Theme theme = currentTheme();
font = theme.getFont();
color = theme.getColor(1);
disabledColor = theme.getColor(7);
backgroundColor = theme.getColor(4);
selectionColor = theme.getColor(4);
selectionBackgroundColor = theme.getColor(14);
inactiveSelectionColor = theme.getColor(1);
inactiveSelectionBackgroundColor = theme.getColor(10);
highlightColor = theme.getColor(1);
highlightBackgroundColor = theme.getColor(10);
spacing = 6;
indent = 16;
showHighlight = true;
showBranchControls = true;
showEmptyBranchControls = true;
branchControlColor = theme.getColor(12);
branchControlSelectionColor = theme.getColor(4);
branchControlInactiveSelectionColor = theme.getColor(14);
gridColor = theme.getColor(11);
showGridLines = false;
}
@Override
public void install(Component component) {
super.install(component);
TreeView treeView = (TreeView) component;
treeView.getTreeViewListeners().add(this);
treeView.getTreeViewBranchListeners().add(this);
treeView.getTreeViewNodeListeners().add(this);
treeView.getTreeViewNodeStateListeners().add(this);
treeView.getTreeViewSelectionListeners().add(this);
treeDataChanged(treeView, null);
}
@Override
public int getPreferredWidth(int height) {
TreeView treeView = (TreeView) getComponent();
TreeView.NodeRenderer nodeRenderer = treeView.getNodeRenderer();
int preferredWidth = 0;
VisibleNodeIterator visibleNodeIterator = new VisibleNodeIterator();
while (visibleNodeIterator.hasNext()) {
NodeInfo nodeInfo = visibleNodeIterator.next();
int nodeWidth = (nodeInfo.depth - 1) * (indent + spacing);
nodeRenderer.render(nodeInfo.data, visibleNodeIterator.getPath(),
visibleNodeIterator.getRowIndex(), treeView, false, false,
TreeView.NodeCheckState.UNCHECKED, false, false);
nodeWidth += nodeRenderer.getPreferredWidth(-1);
preferredWidth = Math.max(preferredWidth, nodeWidth);
}
if (showBranchControls) {
preferredWidth += indent + spacing;
}
if (treeView.getCheckmarksEnabled()) {
preferredWidth += Math.max(CHECKBOX.getWidth(), indent) + spacing;
}
return preferredWidth;
}
@Override
public int getPreferredHeight(int width) {
int nodeHeight = getNodeHeight();
int visibleNodeCount = visibleNodes.getLength();
int preferredHeight = nodeHeight * visibleNodeCount;
if (visibleNodeCount > 1) {
preferredHeight += VERTICAL_SPACING * (visibleNodeCount - 1);
}
return preferredHeight;
}
@Override
public int getBaseline(int width, int height) {
int baseline = -1;
if (visibleNodes.getLength() > 0) {
TreeView treeView = (TreeView) getComponent();
TreeView.NodeRenderer nodeRenderer = treeView.getNodeRenderer();
NodeInfo nodeInfo = visibleNodes.get(0);
int nodeWidth = width - (nodeInfo.depth - 1) * (indent + spacing);
int nodeHeight = getNodeHeight();
boolean expanded = false;
boolean selected = nodeInfo.isSelected();
boolean highlighted = nodeInfo.isHighlighted();
boolean disabled = nodeInfo.isDisabled();
if (showBranchControls) {
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
expanded = branchInfo.isExpanded();
}
nodeWidth -= (indent + spacing);
}
TreeView.NodeCheckState checkState = TreeView.NodeCheckState.UNCHECKED;
if (treeView.getCheckmarksEnabled()) {
checkState = nodeInfo.getCheckState();
nodeWidth -= (Math.max(indent, CHECKBOX.getWidth()) + spacing);
}
nodeRenderer.render(nodeInfo.data, nodeInfo.getPath(), 0, treeView, expanded, selected,
checkState, highlighted, disabled);
baseline = nodeRenderer.getBaseline(nodeWidth, nodeHeight);
}
return baseline;
}
@Override
public void layout() {
if (validateSelection) {
// Ensure that the selection is visible
scrollSelectionToVisible();
}
validateSelection = false;
}
@Override
public void paint(Graphics2D graphics) {
TreeView treeView = (TreeView) getComponent();
TreeView.NodeRenderer nodeRenderer = treeView.getNodeRenderer();
int width = getWidth();
int height = getHeight();
int nodeHeight = getNodeHeight();
// Paint the background
if (backgroundColor != null) {
graphics.setPaint(backgroundColor);
graphics.fillRect(0, 0, width, height);
}
// nodeStart and nodeEnd are both inclusive
int nodeStart = 0;
int nodeEnd = visibleNodes.getLength() - 1;
// Ensure that we only paint items that are visible
Rectangle clipBounds = graphics.getClipBounds();
if (clipBounds != null) {
nodeStart = Math.max(nodeStart,
(int) (clipBounds.y / (double) (nodeHeight + VERTICAL_SPACING)));
nodeEnd = Math.min(
nodeEnd,
(int) ((clipBounds.y + clipBounds.height) / (double) (nodeHeight + VERTICAL_SPACING)));
}
int nodeY = nodeStart * (nodeHeight + VERTICAL_SPACING);
VisibleNodeIterator visibleNodeIterator = new VisibleNodeIterator(nodeStart, nodeEnd);
while (visibleNodeIterator.hasNext()) {
NodeInfo nodeInfo = visibleNodeIterator.next();
boolean expanded = false;
boolean highlighted = nodeInfo.isHighlighted();
boolean selected = nodeInfo.isSelected();
boolean disabled = nodeInfo.isDisabled();
int nodeX = (nodeInfo.depth - 1) * (indent + spacing);
if (treeView.isEnabled()) {
if (selected) {
// Paint the selection state
Color selectionBackgroundColorLocal = treeView.isFocused() ? this.selectionBackgroundColor
: inactiveSelectionBackgroundColor;
graphics.setPaint(selectionBackgroundColorLocal);
graphics.fillRect(0, nodeY, width, nodeHeight);
} else if (highlighted && !disabled) {
// Paint the highlight state
graphics.setPaint(highlightBackgroundColor);
graphics.fillRect(0, nodeY, width, nodeHeight);
}
}
// Paint the expand/collapse control
if (showBranchControls) {
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
boolean showBranchControl = true;
if (!showEmptyBranchControls) {
branchInfo.loadChildren();
showBranchControl = !(branchInfo.children == null || branchInfo.children.isEmpty());
}
if (showBranchControl) {
expanded = branchInfo.isExpanded();
Color branchControlColorLocal;
if (selected) {
if (treeView.isFocused()) {
branchControlColorLocal = branchControlSelectionColor;
} else {
branchControlColorLocal = branchControlInactiveSelectionColor;
}
} else {
branchControlColorLocal = this.branchControlColor;
}
GeneralPath shape = new GeneralPath();
int imageX = nodeX + (indent - BRANCH_CONTROL_IMAGE_WIDTH) / 2;
int imageY = nodeY + (nodeHeight - BRANCH_CONTROL_IMAGE_HEIGHT) / 2;
if (expanded) {
shape.moveTo(imageX, imageY + 1);
shape.lineTo(imageX + 8, imageY + 1);
shape.lineTo(imageX + 4, imageY + 7);
} else {
shape.moveTo(imageX + 1, imageY);
shape.lineTo(imageX + 7, imageY + 4);
shape.lineTo(imageX + 1, imageY + 8);
}
shape.closePath();
Graphics2D branchControlGraphics = (Graphics2D) graphics.create();
GraphicsUtilities.setAntialiasingOn(branchControlGraphics);
if (!treeView.isEnabled() || disabled) {
branchControlGraphics.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 0.5f));
}
branchControlGraphics.setPaint(branchControlColorLocal);
branchControlGraphics.fill(shape);
branchControlGraphics.dispose();
}
}
nodeX += indent + spacing;
}
// Paint the checkbox
TreeView.NodeCheckState checkState = TreeView.NodeCheckState.UNCHECKED;
if (treeView.getCheckmarksEnabled()) {
checkState = nodeInfo.getCheckState();
int checkboxWidth = CHECKBOX.getWidth();
int checkboxHeight = CHECKBOX.getHeight();
int checkboxX = Math.max(indent - checkboxWidth, 0) / 2;
int checkboxY = (nodeHeight - checkboxHeight) / 2;
Graphics2D checkboxGraphics = (Graphics2D) graphics.create(nodeX + checkboxX, nodeY
+ checkboxY, checkboxWidth, checkboxHeight);
Button.State state;
switch (checkState) {
case CHECKED:
state = Button.State.SELECTED;
break;
case MIXED:
state = Button.State.MIXED;
break;
default:
state = Button.State.UNSELECTED;
break;
}
CHECKBOX.setState(state);
CHECKBOX.setEnabled(treeView.isEnabled() && !disabled
&& !nodeInfo.isCheckmarkDisabled());
CHECKBOX.paint(checkboxGraphics);
checkboxGraphics.dispose();
nodeX += Math.max(indent, checkboxWidth) + spacing;
}
int nodeWidth = Math.max(width - nodeX, 0);
// Paint the node data
Graphics2D rendererGraphics = (Graphics2D) graphics.create(nodeX, nodeY, nodeWidth,
nodeHeight);
nodeRenderer.render(nodeInfo.data, visibleNodeIterator.getPath(),
visibleNodeIterator.getRowIndex(), treeView, expanded, selected, checkState,
highlighted, disabled);
nodeRenderer.setSize(nodeWidth, nodeHeight);
nodeRenderer.paint(rendererGraphics);
rendererGraphics.dispose();
// Paint the grid line
if (showGridLines) {
graphics.setPaint(gridColor);
GraphicsUtilities.drawLine(graphics, 0, nodeY + nodeHeight, width,
Orientation.HORIZONTAL);
}
nodeY += nodeHeight + VERTICAL_SPACING;
}
}
public Font getFont() {
return font;
}
public void setFont(Font font) {
Utils.checkNull(font, "font");
this.font = font;
invalidateComponent();
}
public final void setFont(String font) {
setFont(decodeFont(font));
}
public final void setFont(Dictionary<String, ?> font) {
setFont(Theme.deriveFont(font));
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
Utils.checkNull(color, "color");
this.color = color;
repaintComponent();
}
public void setColor(String color) {
setColor(GraphicsUtilities.decodeColor(color, "color"));
}
public final void setColor(int color) {
Theme theme = currentTheme();
setColor(theme.getColor(color));
}
public Color getDisabledColor() {
return disabledColor;
}
public void setDisabledColor(Color disabledColor) {
Utils.checkNull(disabledColor, "disabledColor");
this.disabledColor = disabledColor;
repaintComponent();
}
public void setDisabledColor(String disabledColor) {
setDisabledColor(GraphicsUtilities.decodeColor(disabledColor, "disabledColor"));
}
public final void setDisabledColor(int disabledColor) {
Theme theme = currentTheme();
setDisabledColor(theme.getColor(disabledColor));
}
public Color getBackgroundColor() {
return backgroundColor;
}
public void setBackgroundColor(Color backgroundColor) {
// We allow a null background color here
this.backgroundColor = backgroundColor;
repaintComponent();
}
public void setBackgroundColor(String backgroundColor) {
setBackgroundColor(GraphicsUtilities.decodeColor(backgroundColor, "backgroundColor"));
}
public final void setBackgroundColor(int backgroundColor) {
Theme theme = currentTheme();
setBackgroundColor(theme.getColor(backgroundColor));
}
public Color getSelectionColor() {
return selectionColor;
}
public void setSelectionColor(Color selectionColor) {
Utils.checkNull(selectionColor, "selectionColor");
this.selectionColor = selectionColor;
repaintComponent();
}
public void setSelectionColor(String selectionColor) {
setSelectionColor(GraphicsUtilities.decodeColor(selectionColor, "selectionColor"));
}
public final void setSelectionColor(int selectionColor) {
Theme theme = currentTheme();
setSelectionColor(theme.getColor(selectionColor));
}
public Color getSelectionBackgroundColor() {
return selectionBackgroundColor;
}
public void setSelectionBackgroundColor(Color selectionBackgroundColor) {
Utils.checkNull(selectionBackgroundColor, "selectionBackgroundColor");
this.selectionBackgroundColor = selectionBackgroundColor;
repaintComponent();
}
public void setSelectionBackgroundColor(String selectionBackgroundColor) {
setSelectionBackgroundColor(
GraphicsUtilities.decodeColor(selectionBackgroundColor, "selectionBackgroundColor"));
}
public final void setSelectionBackgroundColor(int selectionBackgroundColor) {
Theme theme = currentTheme();
setSelectionBackgroundColor(theme.getColor(selectionBackgroundColor));
}
public Color getInactiveSelectionColor() {
return inactiveSelectionColor;
}
public void setInactiveSelectionColor(Color inactiveSelectionColor) {
Utils.checkNull(inactiveSelectionColor, "inactiveSelectionColor");
this.inactiveSelectionColor = inactiveSelectionColor;
repaintComponent();
}
public void setInactiveSelectionColor(String inactiveSelectionColor) {
setInactiveSelectionColor(
GraphicsUtilities.decodeColor(inactiveSelectionColor, "inactiveSelectionColor"));
}
public final void setInactiveSelectionColor(int inactiveSelectionColor) {
Theme theme = currentTheme();
setInactiveSelectionColor(theme.getColor(inactiveSelectionColor));
}
public Color getInactiveSelectionBackgroundColor() {
return inactiveSelectionBackgroundColor;
}
public void setInactiveSelectionBackgroundColor(Color inactiveSelectionBackgroundColor) {
Utils.checkNull(inactiveSelectionBackgroundColor, "inactiveSelectionBackgroundColor");
this.inactiveSelectionBackgroundColor = inactiveSelectionBackgroundColor;
repaintComponent();
}
public void setInactiveSelectionBackgroundColor(String inactiveSelectionBackgroundColor) {
setInactiveSelectionBackgroundColor(
GraphicsUtilities.decodeColor(inactiveSelectionBackgroundColor, "inactiveSelectionBackgroundColor"));
}
public final void setInactiveSelectionBackgroundColor(int inactiveSelectionBackgroundColor) {
Theme theme = currentTheme();
setInactiveSelectionBackgroundColor(theme.getColor(inactiveSelectionBackgroundColor));
}
public Color getHighlightColor() {
return highlightColor;
}
public void setHighlightColor(Color highlightColor) {
Utils.checkNull(highlightColor, "highlightColor");
this.highlightColor = highlightColor;
repaintComponent();
}
public void setHighlightColor(String highlightColor) {
setHighlightColor(GraphicsUtilities.decodeColor(highlightColor, "highlightColor"));
}
public final void setHighlightColor(int highlightColor) {
Theme theme = currentTheme();
setHighlightColor(theme.getColor(highlightColor));
}
public Color getHighlightBackgroundColor() {
return highlightBackgroundColor;
}
public void setHighlightBackgroundColor(Color highlightBackgroundColor) {
Utils.checkNull(highlightBackgroundColor, "highlightBackgroundColor");
this.highlightBackgroundColor = highlightBackgroundColor;
repaintComponent();
}
public void setHighlightBackgroundColor(String highlightBackgroundColor) {
setHighlightBackgroundColor(
GraphicsUtilities.decodeColor(highlightBackgroundColor, "highlightBackgroundColor"));
}
public final void setHighlightBackgroundColor(int highlightBackgroundColor) {
Theme theme = currentTheme();
setHighlightBackgroundColor(theme.getColor(highlightBackgroundColor));
}
public int getSpacing() {
return spacing;
}
public void setSpacing(int spacing) {
Utils.checkNonNegative(spacing, "spacing");
this.spacing = spacing;
invalidateComponent();
}
public void setSpacing(Number spacing) {
Utils.checkNull(spacing, "spacing");
setSpacing(spacing.intValue());
}
public int getIndent() {
return indent;
}
public void setIndent(int indent) {
Utils.checkNonNegative(indent, "indent");
this.indent = indent;
invalidateComponent();
}
public void setIndent(Number indent) {
Utils.checkNull(indent, "indent");
setIndent(indent.intValue());
}
public boolean getShowHighlight() {
return showHighlight;
}
public void setShowHighlight(boolean showHighlight) {
this.showHighlight = showHighlight;
repaintComponent();
}
public boolean getShowBranchControls() {
return showBranchControls;
}
public void setShowBranchControls(boolean showBranchControls) {
this.showBranchControls = showBranchControls;
invalidateComponent();
}
public boolean getShowEmptyBranchControls() {
return showEmptyBranchControls;
}
public void setShowEmptyBranchControls(boolean showEmptyBranchControls) {
this.showEmptyBranchControls = showEmptyBranchControls;
repaintComponent();
}
public Color getBranchControlColor() {
return branchControlColor;
}
public void setBranchControlColor(Color branchControlColor) {
Utils.checkNull(branchControlColor, "branchControlColor");
this.branchControlColor = branchControlColor;
repaintComponent();
}
public void setBranchControlColor(String branchControlColor) {
setBranchControlColor(GraphicsUtilities.decodeColor(branchControlColor, "branchControlColor"));
}
public final void setBranchControlColor(int branchControlColor) {
Theme theme = currentTheme();
setBranchControlColor(theme.getColor(branchControlColor));
}
public Color getBranchControlSelectionColor() {
return branchControlSelectionColor;
}
public void setBranchControlSelectionColor(Color branchControlSelectionColor) {
Utils.checkNull(branchControlSelectionColor, "branchControlSelectionColor");
this.branchControlSelectionColor = branchControlSelectionColor;
repaintComponent();
}
public void setBranchControlSelectionColor(String branchControlSelectionColor) {
setBranchControlSelectionColor(
GraphicsUtilities.decodeColor(branchControlSelectionColor, "branchControlSelectionColor"));
}
public final void setBranchControlSelectionColor(int branchControlSelectionColor) {
Theme theme = currentTheme();
setBranchControlSelectionColor(theme.getColor(branchControlSelectionColor));
}
public Color getBranchControlInactiveSelectionColor() {
return branchControlInactiveSelectionColor;
}
public void setBranchControlInactiveSelectionColor(Color branchControlInactiveSelectionColor) {
Utils.checkNull(branchControlInactiveSelectionColor, "branchControlInactiveSelectionColor");
this.branchControlInactiveSelectionColor = branchControlInactiveSelectionColor;
repaintComponent();
}
public void setBranchControlInactiveSelectionColor(String branchControlInactiveSelectionColor) {
setBranchControlInactiveSelectionColor(
GraphicsUtilities.decodeColor(branchControlInactiveSelectionColor, "branchControlInactiveSelectionColor"));
}
public final void setBranchControlInactiveSelectionColor(int branchControlInactiveSelectionColor) {
Theme theme = currentTheme();
setBranchControlInactiveSelectionColor(theme.getColor(branchControlInactiveSelectionColor));
}
public Color getGridColor() {
return gridColor;
}
public void setGridColor(Color gridColor) {
Utils.checkNull(gridColor, "gridColor");
this.gridColor = gridColor;
repaintComponent();
}
public void setGridColor(String gridColor) {
setGridColor(GraphicsUtilities.decodeColor(gridColor, "gridColor"));
}
public final void setGridColor(int gridColor) {
Theme theme = currentTheme();
setGridColor(theme.getColor(gridColor));
}
public boolean getShowGridLines() {
return showGridLines;
}
public void setShowGridLines(boolean showGridLines) {
this.showGridLines = showGridLines;
repaintComponent();
}
/**
* @return The fixed node height of this skin.
*/
protected int getNodeHeight() {
TreeView treeView = (TreeView) getComponent();
TreeView.NodeRenderer nodeRenderer = treeView.getNodeRenderer();
nodeRenderer.render(null, null, -1, treeView, false, false,
TreeView.NodeCheckState.UNCHECKED, false, false);
int nodeHeight = nodeRenderer.getPreferredHeight(-1);
if (treeView.getCheckmarksEnabled()) {
nodeHeight = Math.max(CHECKBOX.getHeight() + (2 * CHECKBOX_VERTICAL_PADDING),
nodeHeight);
}
return nodeHeight;
}
/**
* @return The metadata associated with the node found at the specified
* y-coordinate, or <tt>null</tt> if there is no node at that location.
* @param y The current Y location.
*/
protected final NodeInfo getNodeInfoAt(int y) {
NodeInfo nodeInfo = null;
int nodeHeight = getNodeHeight();
int index = y / (nodeHeight + VERTICAL_SPACING);
if (index >= 0 && index < visibleNodes.getLength()) {
nodeInfo = visibleNodes.get(index);
}
return nodeInfo;
}
/**
* @return The metadata associated with the node at the specified path. The
* path must be valid. The empty path is supported and represents the root
* node info.
* @param path The path to query.
*/
protected final NodeInfo getNodeInfoAt(Path path) {
assert (path != null) : "Path is null";
NodeInfo result = null;
int n = path.getLength();
if (n == 0) {
result = rootBranchInfo;
} else {
BranchInfo branchInfo = rootBranchInfo;
for (int i = 0; i < n - 1; i++) {
branchInfo.loadChildren();
NodeInfo nodeInfo = branchInfo.children.get(path.get(i));
assert (nodeInfo instanceof BranchInfo) : "Invalid path";
branchInfo = (BranchInfo) nodeInfo;
}
branchInfo.loadChildren();
result = branchInfo.children.get(path.get(n - 1));
}
return result;
}
/**
* @return The bounding box defined by the specified node, or <tt>null</tt> if
* the node is not currently visible.
* @param nodeInfo The node information to search for.
*/
protected final Bounds getNodeBounds(NodeInfo nodeInfo) {
Bounds bounds = null;
int index = visibleNodes.indexOf(nodeInfo);
if (index >= 0) {
int nodeHeight = getNodeHeight();
int nodeY = index * (nodeHeight + VERTICAL_SPACING);
bounds = new Bounds(0, nodeY, getWidth(), nodeHeight);
}
return bounds;
}
/**
* Accepts the specified visitor on all node info objects that exist in this
* skin's node info hierarchy.
*
* @param visitor The callback to execute on each node info object
*/
protected final void accept(NodeInfoVisitor visitor) {
Sequence<NodeInfo> nodes = new ArrayList<>();
nodes.add(rootBranchInfo);
while (nodes.getLength() > 0) {
NodeInfo nodeInfo = nodes.get(0);
nodes.remove(0, 1);
visitor.visit(nodeInfo);
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
if (branchInfo.children != null) {
for (int i = 0, n = branchInfo.children.getLength(); i < n; i++) {
nodes.insert(branchInfo.children.get(i), i);
}
}
}
}
}
/**
* Adds all children of the specified branch to the visible node list. Any
* children nodes that are expanded [branches] will also have their children
* made visible, and so on. Invalidates the component only if necessary.
*/
private void addVisibleNodes(BranchInfo parentBranchInfo) {
int insertIndex = -1;
if (parentBranchInfo == rootBranchInfo) {
// Bootstrap case since the root branch is implicitly expanded
insertIndex = 0;
} else {
int branchIndex = visibleNodes.indexOf(parentBranchInfo);
if (branchIndex >= 0) {
insertIndex = branchIndex + 1;
}
}
if (insertIndex >= 0) {
Sequence<NodeInfo> nodes = new ArrayList<>();
// The parent branch's children are the baseline nodes to make visible
parentBranchInfo.loadChildren();
for (int i = 0, n = parentBranchInfo.children.getLength(); i < n; i++) {
nodes.add(parentBranchInfo.children.get(i));
}
while (nodes.getLength() > 0) {
NodeInfo nodeInfo = nodes.get(0);
nodes.remove(0, 1);
visibleNodes.insert(nodeInfo, insertIndex++);
// If we encounter an expanded branch, we add that branch's
// children to our list of nodes that are to become visible
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
if (branchInfo.isExpanded()) {
branchInfo.loadChildren();
for (int i = 0, n = branchInfo.children.getLength(); i < n; i++) {
nodes.insert(branchInfo.children.get(i), i);
}
}
}
}
invalidateComponent();
}
}
/**
* Adds the specified child of the specified branch to the visible node
* list. It is assumed that the child in question is not an expanded branch.
* Invalidates the component only if necessary.
*
* @param parentBranchInfo The branch info of the parent node.
* @param index The index of the child within its parent.
*/
private void addVisibleNode(BranchInfo parentBranchInfo, int index) {
parentBranchInfo.loadChildren();
assert (index >= 0) : "Index is too small";
assert (index < parentBranchInfo.children.getLength()) : "Index is too large";
int branchIndex = visibleNodes.indexOf(parentBranchInfo);
if (parentBranchInfo == rootBranchInfo
|| (branchIndex >= 0 && parentBranchInfo.isExpanded())) {
NodeInfo nodeInfo = parentBranchInfo.children.get(index);
int insertIndex = branchIndex + index + 1;
if (index > 0) {
// Siblings of the node that lie before it may be expanded
// branches, thus adding their own children to the
// visible nodes list and pushing down our insert index
NodeInfo youngerSibling = parentBranchInfo.children.get(index - 1);
// Try to insert after our younger sibling
insertIndex = visibleNodes.indexOf(youngerSibling) + 1;
// Continue looking as long as the node at our insert index
// has a greater depth than we do, which means that it's a
// descendant of our younger sibling
for (int n = visibleNodes.getLength(), nodeDepth = youngerSibling.depth; insertIndex < n
&& visibleNodes.get(insertIndex).depth > nodeDepth; insertIndex++) {
continue;
}
}
visibleNodes.insert(nodeInfo, insertIndex);
invalidateComponent();
}
}
/**
* Removes the specified children of the specified branch from the visible
* node list if necessary. If they are not already in the visible node list,
* nothing happens. Invalidates the component only if necessary.
*
* @param parentBranchInfo The branch info of the parent node.
* @param index The index of the first child node to remove from the visible
* nodes sequence.
* @param count The number of child nodes to remove, or <tt>-1</tt> to remove
* all child nodes from the visible nodes sequence.
*/
private void removeVisibleNodes(BranchInfo parentBranchInfo, int index, int count) {
parentBranchInfo.loadChildren();
int childrenLength = parentBranchInfo.children.getLength();
int countUpdated = count;
if (countUpdated == -1) {
assert (index == 0) : "Non-zero index with 'remove all' count";
countUpdated = childrenLength;
}
// If the index is greater-equal the child length, then there could
// not possibly be any visible nodes, so just quit
if (index >= childrenLength) {
return;
}
assert (index + countUpdated <= childrenLength) : "Value too big";
if (countUpdated > 0) {
NodeInfo first = parentBranchInfo.children.get(index);
NodeInfo last = parentBranchInfo.children.get(index + countUpdated - 1);
int rangeStart = visibleNodes.indexOf(first);
if (rangeStart >= 0) {
int rangeEnd = visibleNodes.indexOf(last) + 1;
assert (rangeEnd > rangeStart) : "Invalid visible node structure";
// Continue looking as long as the node at our endpoint has a
// greater depth than the last child node, which means that
// it's a descendant of the last child node
for (int n = visibleNodes.getLength(), nodeDepth = last.depth; rangeEnd < n
&& visibleNodes.get(rangeEnd).depth > nodeDepth; rangeEnd++) {
continue;
}
visibleNodes.remove(rangeStart, rangeEnd - rangeStart);
invalidateComponent();
}
}
}
/**
* Repaints the region occupied by the specified node.
*
* @param nodeInfo The node to search for.
*/
protected void repaintNode(NodeInfo nodeInfo) {
Bounds bounds = getNodeBounds(nodeInfo);
if (bounds != null) {
repaintComponent(bounds);
}
}
/**
* Clears the highlighted node if one exists.
*/
protected void clearHighlightedNode() {
if (highlightedNode != null) {
highlightedNode.setHighlighted(false);
repaintNode(highlightedNode);
highlightedNode = null;
}
}
/**
* Clears our <tt>NodeInfo</tt> hierarchy of the specified cached field.
*
* @param mask The bitmask specifying which field to clear.
*/
private void clearFields(final byte mask) {
accept(new NodeInfoVisitor() {
@Override
public void visit(NodeInfo nodeInfo) {
nodeInfo.clearField(mask);
}
});
}
/**
* Scrolls the last visible (expanded) selected node into viewport
* visibility. If no such node exists, nothing happens. <p> This should only
* be called when the tree view is valid.
*/
private void scrollSelectionToVisible() {
TreeView treeView = (TreeView) getComponent();
Sequence<Path> selectedPaths = treeView.getSelectedPaths();
int n = selectedPaths.getLength();
if (n > 0) {
Bounds nodeBounds = null;
for (int i = n - 1; i >= 0 && nodeBounds == null; i--) {
NodeInfo nodeInfo = getNodeInfoAt(selectedPaths.get(i));
nodeBounds = getNodeBounds(nodeInfo);
}
if (nodeBounds != null) {
Bounds visibleSelectionBounds = treeView.getVisibleArea(nodeBounds);
if (visibleSelectionBounds != null
&& visibleSelectionBounds.height < nodeBounds.height) {
treeView.scrollAreaToVisible(nodeBounds);
}
}
}
}
@Override
public boolean mouseMove(Component component, int x, int y) {
boolean consumed = super.mouseMove(component, x, y);
TreeView treeView = (TreeView) getComponent();
if (showHighlight && treeView.getSelectMode() != SelectMode.NONE) {
NodeInfo previousHighlightedNode = highlightedNode;
highlightedNode = getNodeInfoAt(y);
if (highlightedNode != previousHighlightedNode) {
if (previousHighlightedNode != null) {
previousHighlightedNode.setHighlighted(false);
repaintNode(previousHighlightedNode);
}
if (highlightedNode != null) {
highlightedNode.setHighlighted(true);
repaintNode(highlightedNode);
}
}
}
return consumed;
}
@Override
public void mouseOut(Component component) {
super.mouseOut(component);
clearHighlightedNode();
selectPath = null;
}
@Override
public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
boolean consumed = super.mouseDown(component, button, x, y);
if (!consumed) {
TreeView treeView = (TreeView) getComponent();
NodeInfo nodeInfo = getNodeInfoAt(y);
if (nodeInfo != null && !nodeInfo.isDisabled()) {
int nodeHeight = getNodeHeight();
int baseNodeX = (nodeInfo.depth - 1) * (indent + spacing);
int nodeX = baseNodeX + (showBranchControls ? indent + spacing : 0);
int nodeY = (y / (nodeHeight + VERTICAL_SPACING)) * (nodeHeight + VERTICAL_SPACING);
int checkboxWidth = CHECKBOX.getWidth();
int checkboxHeight = CHECKBOX.getHeight();
int checkboxX = Math.max(indent - checkboxWidth, 0) / 2;
int checkboxY = (nodeHeight - checkboxHeight) / 2;
// Only proceed if the user DIDN'T click on a checkbox
if (!treeView.getCheckmarksEnabled() || nodeInfo.isCheckmarkDisabled()
|| x < nodeX + checkboxX || x >= nodeX + checkboxX + checkboxWidth
|| y < nodeY + checkboxY || y >= nodeY + checkboxY + checkboxHeight) {
Path path = nodeInfo.getPath();
// See if the user clicked on an expand/collapse control of
// a branch. If so, expand/collapse the branch
if (showBranchControls && nodeInfo instanceof BranchInfo && x >= baseNodeX
&& x < baseNodeX + indent) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
treeView.setBranchExpanded(path, !branchInfo.isExpanded());
consumed = true;
}
// If we haven't consumed the event, then proceed to manage
// the selection state of the node
if (!consumed) {
SelectMode selectMode = treeView.getSelectMode();
if (button == Mouse.Button.LEFT) {
Modifier commandModifier = Platform.getCommandModifier();
if (Keyboard.isPressed(commandModifier)
&& selectMode == SelectMode.MULTI) {
// Toggle the item's selection state
if (nodeInfo.isSelected()) {
treeView.removeSelectedPath(path);
} else {
treeView.addSelectedPath(path);
}
} else if (Keyboard.isPressed(commandModifier)
&& selectMode == SelectMode.SINGLE) {
// Toggle the item's selection state
if (nodeInfo.isSelected()) {
treeView.clearSelection();
} else {
treeView.setSelectedPath(path);
}
} else {
if (selectMode != SelectMode.NONE) {
if (nodeInfo.isSelected()) {
selectPath = path;
} else {
treeView.setSelectedPath(path);
}
}
}
}
}
}
}
treeView.requestFocus();
}
return consumed;
}
@Override
public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
boolean consumed = super.mouseUp(component, button, x, y);
TreeView treeView = (TreeView) getComponent();
if (selectPath != null
&& !treeView.getFirstSelectedPath().equals(treeView.getLastSelectedPath())) {
treeView.setSelectedPath(selectPath);
selectPath = null;
}
return consumed;
}
@Override
public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
boolean consumed = super.mouseClick(component, button, x, y, count);
if (!consumed) {
TreeView treeView = (TreeView) getComponent();
NodeInfo nodeInfo = getNodeInfoAt(y);
if (nodeInfo != null && !nodeInfo.isDisabled()) {
int nodeHeight = getNodeHeight();
int baseNodeX = (nodeInfo.depth - 1) * (indent + spacing);
int nodeX = baseNodeX + (showBranchControls ? indent + spacing : 0);
int nodeY = (y / (nodeHeight + VERTICAL_SPACING)) * (nodeHeight + VERTICAL_SPACING);
int checkboxWidth = CHECKBOX.getWidth();
int checkboxHeight = CHECKBOX.getHeight();
int checkboxX = Math.max(indent - checkboxWidth, 0) / 2;
int checkboxY = (nodeHeight - checkboxHeight) / 2;
if (treeView.getCheckmarksEnabled() && !nodeInfo.isCheckmarkDisabled()
&& x >= nodeX + checkboxX && x < nodeX + checkboxX + checkboxWidth
&& y >= nodeY + checkboxY && y < nodeY + checkboxY + checkboxHeight) {
Path path = nodeInfo.getPath();
treeView.setNodeChecked(path, !nodeInfo.isChecked());
} else {
if (selectPath != null && count == 1 && button == Mouse.Button.LEFT) {
TreeView.NodeEditor nodeEditor = treeView.getNodeEditor();
if (nodeEditor != null) {
if (nodeEditor.isEditing()) {
nodeEditor.endEdit(true);
}
nodeEditor.beginEdit(treeView, selectPath);
}
}
selectPath = null;
}
}
}
return consumed;
}
@Override
public boolean mouseWheel(Component component, Mouse.ScrollType scrollType, int scrollAmount,
int wheelRotation, int x, int y) {
if (highlightedNode != null) {
Bounds nodeBounds = getNodeBounds(highlightedNode);
highlightedNode.setHighlighted(false);
highlightedNode = null;
if (nodeBounds != null) {
repaintComponent(nodeBounds.x, nodeBounds.y, nodeBounds.width, nodeBounds.height,
true);
}
}
return super.mouseWheel(component, scrollType, scrollAmount, wheelRotation, x, y);
}
/**
* Keyboard handling (arrow keys with modifiers).
* <ul>
* <li>{@link KeyCode#UP UP} Selects the previous enabled node when select mode
* is not {@link SelectMode#NONE}</li>
* <li>{@link KeyCode#DOWN DOWN} Selects the next enabled node when select mode
* is not {@link SelectMode#NONE}</li>
* <li>{@link Modifier#SHIFT SHIFT} + {@link KeyCode#UP UP} Increases the
* selection size by including the previous enabled node when select mode is
* {@link SelectMode#MULTI}</li>
* <li>{@link Modifier#SHIFT SHIFT} + {@link KeyCode#DOWN DOWN} Increases the
* selection size by including the next enabled node when select mode is
* {@link SelectMode#MULTI}</li>
* </ul>
*/
@Override
public boolean keyPressed(Component component, int keyCode, KeyLocation keyLocation) {
boolean consumed = false;
TreeView treeView = (TreeView) getComponent();
SelectMode selectMode = treeView.getSelectMode();
switch (keyCode) {
case KeyCode.UP:
if (selectMode != SelectMode.NONE) {
Path firstSelectedPath = treeView.getFirstSelectedPath();
int index;
if (firstSelectedPath != null) {
NodeInfo previousSelectedNode = getNodeInfoAt(firstSelectedPath);
index = visibleNodes.indexOf(previousSelectedNode);
} else {
// Select the last visible node
index = visibleNodes.getLength();
}
NodeInfo newSelectedNode = null;
do {
newSelectedNode = (--index >= 0) ? visibleNodes.get(index) : null;
} while (newSelectedNode != null && newSelectedNode.isDisabled());
if (newSelectedNode != null) {
if (Keyboard.isPressed(Modifier.SHIFT)
&& treeView.getSelectMode() == SelectMode.MULTI) {
treeView.addSelectedPath(newSelectedNode.getPath());
} else {
treeView.setSelectedPath(newSelectedNode.getPath());
}
treeView.scrollAreaToVisible(getNodeBounds(newSelectedNode));
}
consumed = true;
}
break;
case KeyCode.DOWN:
if (selectMode != SelectMode.NONE) {
Path lastSelectedPath = treeView.getLastSelectedPath();
int index;
if (lastSelectedPath != null) {
NodeInfo previousSelectedNode = getNodeInfoAt(lastSelectedPath);
index = visibleNodes.indexOf(previousSelectedNode);
} else {
// Select the first visible node
index = -1;
}
NodeInfo newSelectedNode = null;
int n = visibleNodes.getLength();
do {
newSelectedNode = (++index <= n - 1) ? visibleNodes.get(index) : null;
} while (newSelectedNode != null && newSelectedNode.isDisabled());
if (newSelectedNode != null) {
if (Keyboard.isPressed(Modifier.SHIFT)
&& treeView.getSelectMode() == SelectMode.MULTI) {
treeView.addSelectedPath(newSelectedNode.getPath());
} else {
treeView.setSelectedPath(newSelectedNode.getPath());
}
treeView.scrollAreaToVisible(getNodeBounds(newSelectedNode));
}
consumed = true;
}
break;
case KeyCode.LEFT:
if (showBranchControls) {
Sequence<Path> paths = treeView.getSelectedPaths();
if (paths != null && paths.getLength() > 0) {
Path path = paths.get(paths.getLength() - 1);
NodeInfo nodeInfo = getNodeInfoAt(path);
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
if (branchInfo.isExpanded()) {
treeView.collapseBranch(branchInfo.getPath());
}
}
consumed = true;
}
}
break;
case KeyCode.RIGHT:
if (showBranchControls) {
Sequence<Path> paths = treeView.getSelectedPaths();
if (paths != null && paths.getLength() > 0) {
Path path = paths.get(paths.getLength() - 1);
NodeInfo nodeInfo = getNodeInfoAt(path);
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
if (!branchInfo.isExpanded()) {
treeView.expandBranch(branchInfo.getPath());
}
}
consumed = true;
}
}
break;
default:
consumed = super.keyPressed(component, keyCode, keyLocation);
break;
}
if (consumed) {
clearHighlightedNode();
}
return consumed;
}
/**
* {@link KeyCode#SPACE SPACE} toggles check mark selection when select mode
* is {@link SelectMode#SINGLE}.
*/
@Override
public boolean keyReleased(Component component, int keyCode, KeyLocation keyLocation) {
boolean consumed = false;
TreeView treeView = (TreeView) getComponent();
if (keyCode == KeyCode.SPACE) {
if (treeView.getCheckmarksEnabled()
&& treeView.getSelectMode() == SelectMode.SINGLE) {
Path selectedPath = treeView.getSelectedPath();
if (selectedPath != null) {
NodeInfo nodeInfo = getNodeInfoAt(selectedPath);
if (!nodeInfo.isCheckmarkDisabled()) {
treeView.setNodeChecked(selectedPath, !treeView.isNodeChecked(selectedPath));
}
}
}
} else {
consumed = super.keyReleased(component, keyCode, keyLocation);
}
return consumed;
}
@Override
public boolean isFocusable() {
TreeView treeView = (TreeView) getComponent();
return (treeView.getSelectMode() != SelectMode.NONE);
}
@Override
public boolean isOpaque() {
return (backgroundColor != null && backgroundColor.getTransparency() == Transparency.OPAQUE);
}
// ComponentStateListener methods
@Override
public void enabledChanged(Component component) {
super.enabledChanged(component);
repaintComponent();
}
@Override
public void focusedChanged(Component component, Component obverseComponent) {
super.focusedChanged(component, obverseComponent);
repaintComponent();
}
// TreeView.Skin methods
@Override
public Path getNodeAt(int y) {
Path path = null;
NodeInfo nodeInfo = getNodeInfoAt(y);
if (nodeInfo != null) {
path = nodeInfo.getPath();
}
return path;
}
@Override
public Bounds getNodeBounds(Path path) {
Bounds nodeBounds = null;
NodeInfo nodeInfo = getNodeInfoAt(path);
if (nodeInfo != null) {
nodeBounds = getNodeBounds(nodeInfo);
}
return nodeBounds;
}
@Override
public int getNodeIndent(int depth) {
TreeView treeView = (TreeView) getComponent();
int nodeIndent = (depth - 1) * (indent + spacing);
if (showBranchControls) {
nodeIndent += indent + spacing;
}
if (treeView.getCheckmarksEnabled()) {
nodeIndent += Math.max(CHECKBOX.getWidth(), indent) + spacing;
}
return nodeIndent;
}
@Override
public int getRowIndex(Path path) {
int rowIndex = -1;
NodeInfo nodeInfo = getNodeInfoAt(path);
if (nodeInfo != null) {
rowIndex = visibleNodes.indexOf(nodeInfo);
}
return rowIndex;
}
// TreeViewListener methods
@Override
public void treeDataChanged(TreeView treeView, List<?> previousTreeData) {
@SuppressWarnings("unchecked")
List<Object> treeData = (List<Object>) treeView.getTreeData();
visibleNodes.clear();
if (treeData == null) {
rootBranchInfo = null;
} else {
rootBranchInfo = new BranchInfo(treeView, null, treeData);
addVisibleNodes(rootBranchInfo);
}
invalidateComponent();
}
@Override
public void nodeRendererChanged(TreeView treeView, TreeView.NodeRenderer previousNodeRenderer) {
invalidateComponent();
}
@Override
public void selectModeChanged(TreeView treeView, SelectMode previousSelectMode) {
// The selection has implicitly been cleared
clearFields(NodeInfo.SELECTED_MASK);
repaintComponent();
}
@Override
public void checkmarksEnabledChanged(TreeView treeView) {
// The check state of all nodes has implicitly been cleared
clearFields(NodeInfo.CHECK_STATE_MASK);
invalidateComponent();
}
@Override
public void showMixedCheckmarkStateChanged(TreeView treeView) {
if (treeView.getCheckmarksEnabled()) {
// The check state of all *branch* nodes may have changed, so we
// need to update the cached check state of all BranchNode
// instances in our hierarchy
Sequence<NodeInfo> nodes = new ArrayList<>();
nodes.add(rootBranchInfo);
while (nodes.getLength() > 0) {
NodeInfo nodeInfo = nodes.get(0);
nodes.remove(0, 1);
// Only branch nodes can be affected by this event
if (nodeInfo instanceof BranchInfo) {
BranchInfo branchInfo = (BranchInfo) nodeInfo;
// Update the cached entry for this branch
Path path = branchInfo.getPath();
branchInfo.setCheckState(treeView.getNodeCheckState(path));
// Add the branch's children to the queue
if (branchInfo.children != null) {
for (int i = 0, n = branchInfo.children.getLength(); i < n; i++) {
nodes.insert(branchInfo.children.get(i), i);
}
}
}
}
repaintComponent();
}
}
@Override
public void disabledNodeFilterChanged(TreeView treeView, Filter<?> previousDisabledNodeFilter) {
@SuppressWarnings("unchecked")
final Filter<Object> disabledNodeFilter = (Filter<Object>) treeView.getDisabledNodeFilter();
accept(new NodeInfoVisitor() {
@Override
public void visit(NodeInfo nodeInfo) {
if (nodeInfo != rootBranchInfo) {
nodeInfo.setDisabled(disabledNodeFilter != null
&& disabledNodeFilter.include(nodeInfo.data));
}
}
});
repaintComponent();
}
@Override
public void disabledCheckmarkFilterChanged(TreeView treeView,
Filter<?> previousDisabledCheckmarkFilter) {
@SuppressWarnings("unchecked")
final Filter<Object> disabledCheckmarkFilter = (Filter<Object>) treeView.getDisabledCheckmarkFilter();
accept(new NodeInfoVisitor() {
@Override
public void visit(NodeInfo nodeInfo) {
if (nodeInfo != rootBranchInfo) {
nodeInfo.setCheckmarkDisabled(disabledCheckmarkFilter != null
&& disabledCheckmarkFilter.include(nodeInfo.data));
}
}
});
repaintComponent();
}
// TreeViewBranchListener methods
@Override
public void branchExpanded(TreeView treeView, Path path) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
branchInfo.setExpanded(true);
addVisibleNodes(branchInfo);
repaintNode(branchInfo);
}
@Override
public void branchCollapsed(TreeView treeView, Path path) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
branchInfo.setExpanded(false);
removeVisibleNodes(branchInfo, 0, -1);
repaintNode(branchInfo);
}
@Override
public Vote previewBranchExpandedChange(TreeView treeView, Path path) {
// We currently have no reason to refuse to open / close the branch
// although other listeners might have a reason
return Vote.APPROVE;
}
@Override
public void branchExpandedChangeVetoed(TreeView treeView, Path path, Vote reason) {
// Nothing really to do -- our visual state doesn't change until/unless the
// expand/collapse really happens
}
// TreeViewNodeListener methods
@Override
@SuppressWarnings("unchecked")
public void nodeInserted(TreeView treeView, Path path, int index) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
List<Object> branchData = (List<Object>) branchInfo.data;
// Update our internal branch info
if (branchInfo.children != null) {
NodeInfo nodeInfo = NodeInfo.newInstance(treeView, branchInfo, branchData.get(index));
branchInfo.children.insert(nodeInfo, index);
}
// Add the node to the visible nodes list
addVisibleNode(branchInfo, index);
// If the empty branch controls are not shown, then this event might
// need a repaint of the parent
if (!showEmptyBranchControls) {
repaintNode(branchInfo);
}
}
@Override
public void nodesRemoved(TreeView treeView, Path path, int index, int count) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
// Remove the nodes from the visible nodes list
removeVisibleNodes(branchInfo, index, count);
// Update our internal branch info
if (branchInfo.children != null) {
// Problem: if "loadChildren" was called on this branch the first time
// by "removeVisibleNodes" above, then the "children" will actually be
// correct here (i.e., already removed), so this is unnecessary.
int len = branchInfo.children.getLength();
if (index < len && index + count <= len) {
branchInfo.children.remove(index, count);
}
}
// If the empty branch controls are not shown, then this event might
// need a repaint of the parent
if (!showEmptyBranchControls) {
repaintNode(branchInfo);
}
}
@Override
@SuppressWarnings("unchecked")
public void nodeUpdated(TreeView treeView, Path path, int index) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
List<Object> branchData = (List<Object>) branchInfo.data;
branchInfo.loadChildren();
NodeInfo nodeInfo = branchInfo.children.get(index);
Object previousNodeData = nodeInfo.data;
Object nodeData = branchData.get(index);
if (previousNodeData != nodeData) {
// Remove the old node from the visible nodes list
removeVisibleNodes(branchInfo, index, 1);
// Update our internal branch info
nodeInfo = NodeInfo.newInstance(treeView, branchInfo, nodeData);
branchInfo.children.update(index, nodeInfo);
// Add the new node to the visible nodes list
addVisibleNode(branchInfo, index);
} else {
// This update might affect the node's disabled state
Filter<Object> disabledNodeFilter = (Filter<Object>) treeView.getDisabledNodeFilter();
nodeInfo.setDisabled(disabledNodeFilter != null && disabledNodeFilter.include(nodeData));
if (visibleNodes.indexOf(nodeInfo) >= 0) {
// The updated node data might affect our preferred width
invalidateComponent();
}
}
}
@Override
public void nodesCleared(TreeView treeView, Path path) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
// Remove the node from the visible nodes list
removeVisibleNodes(branchInfo, 0, -1);
// Update our internal branch info
if (branchInfo.children != null) {
branchInfo.children.clear();
}
}
@Override
public void nodesSorted(TreeView treeView, Path path) {
BranchInfo branchInfo = (BranchInfo) getNodeInfoAt(path);
// Remove the child nodes from the visible nodes list
removeVisibleNodes(branchInfo, 0, -1);
// Re-load the branch's children to get the correct sort order
branchInfo.children = null;
branchInfo.loadChildren();
// Add the child nodes back to the visible nodes list
addVisibleNodes(branchInfo);
}
// TreeViewNodeStateListener methods
@Override
public void nodeCheckStateChanged(TreeView treeView, Path path,
TreeView.NodeCheckState previousCheckState) {
NodeInfo nodeInfo = getNodeInfoAt(path);
nodeInfo.setCheckState(treeView.getNodeCheckState(path));
repaintNode(nodeInfo);
}
// TreeViewSelectionListener methods
@Override
public void selectedPathAdded(TreeView treeView, Path path) {
// Update the node info
NodeInfo nodeInfo = getNodeInfoAt(path);
nodeInfo.setSelected(true);
if (treeView.isValid()) {
Bounds nodeBounds = getNodeBounds(nodeInfo);
if (nodeBounds != null) {
// Ensure that the selection is visible
Bounds visibleSelectionBounds = treeView.getVisibleArea(nodeBounds);
if (visibleSelectionBounds.height < nodeBounds.height) {
treeView.scrollAreaToVisible(nodeBounds);
}
}
} else {
validateSelection = true;
}
repaintNode(nodeInfo);
}
@Override
public void selectedPathRemoved(TreeView treeView, Path path) {
NodeInfo nodeInfo = getNodeInfoAt(path);
nodeInfo.setSelected(false);
repaintNode(nodeInfo);
}
@Override
public void selectedPathsChanged(TreeView treeView, Sequence<Path> previousSelectedPaths) {
if (previousSelectedPaths != null && previousSelectedPaths != treeView.getSelectedPaths()) {
// Ensure that the selection is visible
if (treeView.isValid()) {
scrollSelectionToVisible();
} else {
validateSelection = true;
}
// Un-select the previous selected paths
for (int i = 0, n = previousSelectedPaths.getLength(); i < n; i++) {
NodeInfo previousSelectedNode = getNodeInfoAt(previousSelectedPaths.get(i));
previousSelectedNode.setSelected(false);
repaintNode(previousSelectedNode);
}
Sequence<Path> selectedPaths = treeView.getSelectedPaths();
// Select the current selected paths
for (int i = 0, n = selectedPaths.getLength(); i < n; i++) {
NodeInfo selectedNode = getNodeInfoAt(selectedPaths.get(i));
selectedNode.setSelected(true);
repaintNode(selectedNode);
}
}
}
}