// GameTreePanel.java
package net.sf.gogui.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.util.HashMap;
import java.util.HashSet;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import net.sf.gogui.game.ConstNode;
import net.sf.gogui.game.ConstGameTree;
import net.sf.gogui.game.NodeUtil;
import static net.sf.gogui.gui.I18n.i18n;
/** Panel displaying a game tree. */
public class GameTreePanel
extends JPanel
implements Scrollable
{
public enum Label
{
NUMBER,
MOVE,
NONE
}
public enum Size
{
LARGE,
NORMAL,
SMALL,
TINY
}
public static final Color BACKGROUND = new Color(192, 192, 192);
public GameTreePanel(JDialog owner, GameTreeViewer.Listener listener,
Label labelMode, Size sizeMode,
MessageDialogs messageDialogs)
{
super(new SpringLayout());
m_messageDialogs = messageDialogs;
m_owner = owner;
setBackground(BACKGROUND);
m_labelMode = labelMode;
m_sizeMode = sizeMode;
initSize(sizeMode);
setFocusable(false);
setFocusTraversalKeysEnabled(false);
setAutoscrolls(true);
addMouseMotionListener(new GameTreePanel.MouseMotionListener());
m_listener = listener;
m_mouseListener = new MouseAdapter()
{
public void mouseClicked(MouseEvent event)
{
if (event.getButton() != MouseEvent.BUTTON1)
return;
GameTreeNode gameNode = (GameTreeNode)event.getSource();
gotoNode(gameNode.getNode());
}
public void mousePressed(MouseEvent event)
{
if (event.isPopupTrigger())
{
GameTreeNode gameNode
= (GameTreeNode)event.getSource();
int x = event.getX();
int y = event.getY();
showPopup(x, y, gameNode);
}
}
public void mouseReleased(MouseEvent event)
{
if (event.isPopupTrigger())
{
GameTreeNode gameNode
= (GameTreeNode)event.getSource();
int x = event.getX();
int y = event.getY();
showPopup(x, y, gameNode);
}
}
};
}
public ConstNode getCurrentNode()
{
return m_currentNode;
}
public Label getLabelMode()
{
return m_labelMode;
}
public int getNodeFullSize()
{
return m_nodeFullSize;
}
public int getNodeSize()
{
return m_nodeSize;
}
public Dimension getPreferredScrollableViewportSize()
{
return new Dimension(m_nodeFullSize * 10, m_nodeFullSize * 3);
}
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation, int direction)
{
int result;
if (orientation == SwingConstants.VERTICAL)
result = visibleRect.height;
else
result = visibleRect.width;
result = (result / m_nodeFullSize) * m_nodeFullSize;
return result;
}
public boolean getScrollableTracksViewportHeight()
{
return false;
}
public boolean getScrollableTracksViewportWidth()
{
return false;
}
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation, int direction)
{
return m_nodeFullSize;
}
public boolean getShowSubtreeSizes()
{
return m_showSubtreeSizes;
}
public Size getSizeMode()
{
return m_sizeMode;
}
public void gotoNode(ConstNode node)
{
if (m_listener != null)
m_listener.actionGotoNode(node);
}
public boolean isCurrent(ConstNode node)
{
return node == m_currentNode;
}
public boolean isExpanded(ConstNode node)
{
return m_isExpanded.contains(node);
}
public void paintComponent(Graphics graphics)
{
GuiUtil.setAntiAlias(graphics);
super.paintComponent(graphics);
}
public void redrawCurrentNode()
{
GameTreeNode gameNode = getGameTreeNode(m_currentNode);
gameNode.repaint();
}
public void scrollToCurrent()
{
scrollRectToVisible(new Rectangle(m_currentNodeX - 2 * m_nodeSize,
m_currentNodeY - m_nodeSize,
5 * m_nodeSize,
3 * m_nodeSize));
}
public void setLabelMode(Label mode)
{
switch (mode)
{
case NUMBER:
case MOVE:
case NONE:
m_labelMode = mode;
break;
default:
assert false;
break;
}
}
/** Only used for a workaround on Mac Java 1.4.2,
which causes the scrollpane to lose focus after a new layout of
this panel. If scrollPane is not null, a requestFocusOnWindow will
be called after each new layout */
public void setScrollPane(JScrollPane scrollPane)
{
m_scrollPane = scrollPane;
}
public void setShowSubtreeSizes(boolean showSubtreeSizes)
{
m_showSubtreeSizes = showSubtreeSizes;
}
public void setSizeMode(Size mode)
{
switch (mode)
{
case LARGE:
case NORMAL:
case SMALL:
case TINY:
if (mode != m_sizeMode)
{
m_sizeMode = mode;
initSize(m_sizeMode);
}
break;
default:
assert false;
break;
}
}
/** Faster than update if a new node was added as the first child. */
public void addNewSingleChild(ConstNode node)
{
assert ! node.hasChildren();
ConstNode father = node.getFatherConst();
assert father != null;
assert father.getNumberChildren() == 1;
GameTreeNode fatherGameNode = getGameTreeNode(father);
if (fatherGameNode == null)
{
assert false;
return;
}
int moveNumber = NodeUtil.getMoveNumber(node);
GameTreeNode gameNode = createNode(node, moveNumber);
m_map.put(node, gameNode);
add(gameNode);
putConstraint(fatherGameNode, gameNode, m_nodeFullSize, 0);
gameNode.setLocation(fatherGameNode.getX() + m_nodeFullSize,
fatherGameNode.getY());
gameNode.setSize(m_nodeFullSize, m_nodeFullSize);
m_maxX = Math.max(fatherGameNode.getX() + 2 * m_nodeFullSize, m_maxX);
setPreferredSize(new Dimension(m_maxX + m_nodeFullSize + MARGIN,
m_maxY + m_nodeFullSize + MARGIN));
}
public void showPopup()
{
if (m_currentNode == null)
return;
scrollToCurrent();
GameTreeNode gameNode = getGameTreeNode(m_currentNode);
if (gameNode == null)
return;
showPopup(gameNode.getWidth() / 2, gameNode.getHeight() / 2,
gameNode);
}
public void update(ConstGameTree tree, ConstNode currentNode,
int minWidth, int minHeight)
{
assert currentNode != null;
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
m_minWidth = minWidth;
m_minHeight = minHeight;
boolean gameTreeChanged = (tree != m_tree);
if (gameTreeChanged)
m_isExpanded.clear();
ensureVisible(currentNode);
m_tree = tree;
m_currentNode = currentNode;
removeAll();
m_map.clear();
m_maxX = minWidth;
m_maxY = minHeight;
try
{
ConstNode root = m_tree.getRootConst();
createNodes(this, root, 0, 0, MARGIN, MARGIN, 0);
if (gameTreeChanged
&& ! NodeUtil.subtreeGreaterThan(root, 10000))
showSubtree(root);
}
catch (OutOfMemoryError e)
{
m_isExpanded.clear();
removeAll();
m_messageDialogs.showError(m_owner,
i18n("MSG_TREE_OUTOFMEM"),
i18n("MSG_TREE_OUTOFMEM_2"));
update(tree, currentNode, minWidth, minHeight);
}
setPreferredSize(new Dimension(m_maxX + m_nodeFullSize + MARGIN,
m_maxY + m_nodeFullSize + MARGIN));
revalidate();
scrollToCurrent();
if (m_scrollPane != null)
m_scrollPane.requestFocusInWindow();
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
public void update(ConstNode currentNode, int minWidth, int minHeight)
{
assert currentNode != null;
if (ensureVisible(currentNode))
{
update(m_tree, currentNode, minWidth, minHeight);
return;
}
GameTreeNode gameNode = getGameTreeNode(m_currentNode);
if (gameNode == null)
{
// The following warning was previously an assert false.
// But it can can happen, because GoGui does sometimes defer a full
// update of the tree with SwingUtilities::invokeLater to be
// able to show a busy cursor and it can happen that a lightweight
// update (which assumes that the tree structure has not changed)
// is called before the full update event is dispatched.
// In the future, it would be easier to do all full updates in
// the event dispatch tree, even if they take long, but then we
// need another method to show the busy cursor while the UI is
// non-responsive
System.err.println("GameTreePanel: current node not found");
return;
}
gameNode.repaint();
gameNode = getGameTreeNode(currentNode);
if (gameNode == null)
{
update(m_tree, currentNode, minWidth, minHeight);
return;
}
Point location = gameNode.getLocation();
m_currentNodeX = location.x;
m_currentNodeY = location.y;
gameNode.repaint();
gameNode.updateToolTip();
m_currentNode = currentNode;
scrollToCurrent();
if (m_scrollPane != null)
m_scrollPane.requestFocusInWindow();
}
private static class MouseMotionListener
extends MouseMotionAdapter
{
public void mouseDragged(MouseEvent event)
{
int x = event.getX();
int y = event.getY();
JPanel panel = (JPanel)event.getSource();
Rectangle rectangle = new Rectangle(x, y, 1, 1);
panel.scrollRectToVisible(rectangle);
}
}
private boolean m_showSubtreeSizes;
private int m_currentNodeX;
private int m_currentNodeY;
private Label m_labelMode;
private int m_minHeight;
private int m_minWidth;
private Size m_sizeMode;
private int m_nodeSize;
private int m_nodeFullSize;
private static final int MARGIN = 15;
private int m_maxX;
private int m_maxY;
private Dimension m_preferredNodeSize;
private Font m_font;
private ConstGameTree m_tree;
private final GameTreeViewer.Listener m_listener;
private final JDialog m_owner;
/** Used for focus workaround on Mac Java 1.4.2 if not null. */
private JScrollPane m_scrollPane;
private ConstNode m_currentNode;
private ConstNode m_popupNode;
private final HashMap<ConstNode,GameTreeNode> m_map =
new HashMap<ConstNode,GameTreeNode>(500, 0.8f);
private final HashSet<ConstNode> m_isExpanded
= new HashSet<ConstNode>(200);
private final MouseListener m_mouseListener;
private Point m_popupLocation;
private ImageIcon m_iconBlack;
private ImageIcon m_iconWhite;
private ImageIcon m_iconSetup;
private final MessageDialogs m_messageDialogs;
private JPopupMenu m_popup;
private JMenuItem m_itemGoto;
private JMenuItem m_itemScrollToCurrent;
private JMenuItem m_itemHideSubtree;
private JMenuItem m_itemShowSubtree;
private JMenuItem m_itemShowChildren;
private void initSize(Size sizeMode)
{
switch (sizeMode)
{
case LARGE:
m_nodeSize = 32;
m_nodeFullSize = 40;
m_iconBlack = GuiUtil.getIcon("gogui-black-32x32", "");
m_iconWhite = GuiUtil.getIcon("gogui-white-32x32", "");
m_iconSetup = GuiUtil.getIcon("gogui-setup-32x32", "");
break;
case SMALL:
m_nodeSize = 16;
m_nodeFullSize = 20;
m_iconBlack = GuiUtil.getIcon("gogui-black-16x16", "");
m_iconWhite = GuiUtil.getIcon("gogui-white-16x16", "");
m_iconSetup = GuiUtil.getIcon("gogui-setup-16x16", "");
break;
case TINY:
m_nodeSize = 8;
m_nodeFullSize = 10;
m_iconBlack = GuiUtil.getIcon("gogui-black-8x8", "");
m_iconWhite = GuiUtil.getIcon("gogui-white-8x8", "");
m_iconSetup = GuiUtil.getIcon("gogui-setup-8x8", "");
break;
case NORMAL:
m_nodeSize = 24;
m_nodeFullSize = 30;
m_iconBlack = GuiUtil.getIcon("gogui-black-24x24", "");
m_iconWhite = GuiUtil.getIcon("gogui-white-24x24", "");
m_iconSetup = GuiUtil.getIcon("gogui-setup-24x24", "");
}
m_font = new Font("Dialog", Font.PLAIN, (int)(0.4 * m_nodeSize));
m_preferredNodeSize = new Dimension(m_nodeFullSize, m_nodeFullSize);
}
private GameTreeNode createNode(ConstNode node, int moveNumber)
{
return new GameTreeNode(node, moveNumber, this, m_mouseListener,
m_font, m_iconBlack.getImage(),
m_iconWhite.getImage(),
m_iconSetup.getImage(),
m_preferredNodeSize);
}
private int createNodes(Component father, ConstNode node, int x, int y,
int dx, int dy, int moveNumber)
{
m_maxX = Math.max(x, m_maxX);
m_maxY = Math.max(y, m_maxY);
if (node.getMove() != null)
++moveNumber;
GameTreeNode gameNode = createNode(node, moveNumber);
m_map.put(node, gameNode);
add(gameNode);
putConstraint(father, gameNode, dx, dy);
int numberChildren = node.getNumberChildren();
dx = m_nodeFullSize;
dy = 0;
boolean isExpanded = isExpanded(node);
if (isExpanded)
{
int[] childrenDy = new int[numberChildren];
for (int i = 0; i < numberChildren; ++i)
{
childrenDy[i] = dy;
dy += createNodes(gameNode, node.getChildConst(i),
x + dx, y + dy, dx, dy, moveNumber);
if (i < numberChildren - 1)
dy += m_nodeFullSize;
}
if (numberChildren > 1)
{
GameTreeJunction junction =
new GameTreeJunction(childrenDy, this);
add(junction);
putConstraint(gameNode, junction, 0, m_nodeFullSize);
}
}
else
{
if (m_showSubtreeSizes && node.hasChildren())
{
int subtreeSize = NodeUtil.subtreeSize(node) - 1;
String text = Integer.toString(subtreeSize);
// Use upper limit for textWidth
int textWidth = text.length() + m_font.getSize();
int textHeight = m_font.getSize();
int pad = GuiUtil.SMALL_PAD;
m_maxX = Math.max(x + textWidth + pad, m_maxX);
JLabel label = new JLabel(text);
label.setFont(m_font);
add(label);
putConstraint(gameNode, label, dx + pad,
(m_nodeSize - textHeight) / 2);
}
}
if (node == m_currentNode)
{
m_currentNodeX = x;
m_currentNodeY = y;
}
return dy;
}
private void createPopup()
{
m_popup = new JPopupMenu();
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
String command = event.getActionCommand();
if (command.equals("goto"))
gotoNode(m_popupNode);
else if (command.equals("show-variations"))
showChildren(m_popupNode);
else if (command.equals("show-subtree"))
showSubtree(m_popupNode);
else if (command.equals("hide-others"))
hideOthers(m_popupNode);
else if (command.equals("hide-subtree"))
hideSubtree(m_popupNode);
else if (command.equals("node-info"))
nodeInfo(m_popupLocation, m_popupNode);
else if (command.equals("scroll-to-current"))
scrollTo(m_currentNode);
else if (command.equals("tree-info"))
treeInfo(m_popupLocation, m_popupNode);
else if (command.equals("cancel"))
m_popup.setVisible(false);
else
assert false;
}
};
JMenuItem item;
item = new JMenuItem(i18n("MN_TREE_GOTO"));
item.setActionCommand("goto");
item.addActionListener(listener);
m_popup.add(item);
m_itemGoto = item;
item = new JMenuItem(i18n("MN_TREE_SCROLL_TO_CURRENT"));
item.setActionCommand("scroll-to-current");
item.addActionListener(listener);
m_popup.add(item);
m_itemScrollToCurrent = item;
m_popup.addSeparator();
item = new JMenuItem(i18n("MN_TREE_HIDE_SUBTREE"));
m_itemHideSubtree = item;
item.setActionCommand("hide-subtree");
item.addActionListener(listener);
m_popup.add(item);
item = new JMenuItem(i18n("MN_TREE_HIDE_OTHERS"));
item.setActionCommand("hide-others");
item.addActionListener(listener);
m_popup.add(item);
item = new JMenuItem(i18n("MN_TREE_SHOW_CHILDREN"));
m_itemShowChildren = item;
item.setActionCommand("show-variations");
item.addActionListener(listener);
m_popup.add(item);
item = new JMenuItem(i18n("MN_TREE_SHOW_SUBTREE"));
m_itemShowSubtree = item;
item.setActionCommand("show-subtree");
item.addActionListener(listener);
m_popup.add(item);
m_popup.addSeparator();
item = new JMenuItem(i18n("MN_TREE_NODE_INFO"));
item.setActionCommand("node-info");
item.addActionListener(listener);
m_popup.add(item);
item = new JMenuItem(i18n("MN_TREE_SUBTREE_STATISTICS"));
item.setActionCommand("tree-info");
item.addActionListener(listener);
m_popup.add(item);
m_popup.addSeparator();
item = new JMenuItem(i18n("LB_CANCEL"));
item.setActionCommand("cancel");
item.addActionListener(listener);
m_popup.add(item);
}
private GameTreeNode getGameTreeNode(ConstNode node)
{
return m_map.get(node);
}
private boolean ensureVisible(ConstNode node)
{
boolean changed = false;
while (node != null)
{
ConstNode father = node.getFatherConst();
if (father != null)
if (m_isExpanded.add(father))
changed = true;
node = father;
}
return changed;
}
private void hideOthers(ConstNode node)
{
m_isExpanded.clear();
ensureVisible(node);
update(m_tree, m_currentNode, getWidth(), getHeight());
}
private void hideSubtree(ConstNode root)
{
boolean changed = false;
boolean currentChanged = false;
int depth = NodeUtil.getDepth(root);
ConstNode node = root;
while (node != null)
{
if (node == m_currentNode)
{
m_currentNode = root;
currentChanged = true;
changed = true;
}
if (m_isExpanded.remove(node))
changed = true;
node = NodeUtil.nextNode(node, depth);
}
if (currentChanged)
{
gotoNode(m_currentNode);
root = m_currentNode;
}
if (changed)
{
update(m_tree, m_currentNode, getWidth(), getHeight());
scrollTo(root);
}
}
private void nodeInfo(Point location, ConstNode node)
{
String nodeInfo = NodeUtil.nodeInfo(node);
String title = i18n("TIT_NODE_INFO");
TextViewer textViewer = new TextViewer(m_owner, title, nodeInfo, true,
null);
textViewer.setLocation(location);
textViewer.setVisible(true);
}
private void putConstraint(Component father, Component son,
int west, int north)
{
SpringLayout layout = (SpringLayout)getLayout();
layout.putConstraint(SpringLayout.WEST, son, west,
SpringLayout.WEST, father);
layout.putConstraint(SpringLayout.NORTH, son, north,
SpringLayout.NORTH, father);
}
private void scrollTo(ConstNode node)
{
if (node == null)
return;
GameTreeNode gameNode = getGameTreeNode(node);
Rectangle rectangle = new Rectangle();
rectangle.x = gameNode.getLocation().x;
rectangle.y = gameNode.getLocation().y;
// Make rectangle large so that children are visible
rectangle.width = 3 * m_nodeFullSize;
rectangle.height = 3 * m_nodeFullSize;
scrollRectToVisible(rectangle);
}
private void showPopup(int x, int y, GameTreeNode gameNode)
{
ConstNode node = gameNode.getNode();
m_popupNode = node;
if (m_popup == null)
createPopup();
m_itemGoto.setEnabled(node != m_currentNode);
m_itemScrollToCurrent.setEnabled(node != m_currentNode);
boolean hasChildren = node.hasChildren();
m_itemHideSubtree.setEnabled(hasChildren);
m_itemShowSubtree.setEnabled(hasChildren);
m_itemShowChildren.setEnabled(hasChildren);
m_popup.show(gameNode, x, y);
m_popupLocation = m_popup.getLocationOnScreen();
}
private void showSubtree(ConstNode root)
{
if (NodeUtil.subtreeGreaterThan(root, 10000))
{
String mainMessage = i18n("MSG_TREE_EXPAND_LARGE");
String optionalMessage = i18n("MSG_TREE_EXPAND_LARGE_2");
if (! m_messageDialogs.showWarningQuestion(m_owner, mainMessage,
optionalMessage,
i18n("LB_TREE_EXPAND"),
true))
return;
}
boolean changed = false;
ConstNode node = root;
int depth = NodeUtil.getDepth(node);
while (node != null)
{
if (m_isExpanded.add(node))
changed = true;
node = NodeUtil.nextNode(node, depth);
}
if (changed)
{
update(m_tree, m_currentNode, m_minWidth, m_minHeight);
// Game node could have disappeared, because after out of memory
// error all nodes are hidden but main variation
if (getGameTreeNode(root) == null)
{
ensureVisible(root);
update(m_tree, m_currentNode, m_minWidth, m_minHeight);
}
scrollTo(root);
}
}
private void showChildren(ConstNode node)
{
if (m_isExpanded.add(node))
{
update(m_tree, m_currentNode, m_minWidth, m_minHeight);
scrollTo(node);
}
}
private void treeInfo(Point location, ConstNode node)
{
String treeInfo = NodeUtil.treeInfo(node);
String title = i18n("TIT_SUBTREE_INFO");
TextViewer textViewer = new TextViewer(m_owner, title, treeInfo, true,
null);
textViewer.setLocation(location);
textViewer.setVisible(true);
}
}