package games.strategy.triplea.ui.history; import java.awt.BorderLayout; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.Stack; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.history.HistoryNode; import games.strategy.engine.history.Step; import games.strategy.triplea.ui.IUIContext; /** * Shows the history as a tree. */ public class HistoryPanel extends JPanel { private static final long serialVersionUID = -8353246449552215276L; private final GameData m_data; private final JTree m_tree; private final IHistoryDetailsPanel m_details; private HistoryNode m_currentPopupNode; private final JPopupMenu m_popup; // private final UIContext m_uiContext; // private boolean m_lockBefore; public HistoryPanel(final GameData data, final IHistoryDetailsPanel details, final JPopupMenu popup, final IUIContext uiContext) { // m_uiContext = uiContext; m_mouseOverPanel = false; m_mouseWasOverPanel = false; final MouseListener mouseFocusListener = new MouseListener() { @Override public void mouseReleased(final MouseEvent e) {} @Override public void mousePressed(final MouseEvent e) {} @Override public void mouseClicked(final MouseEvent e) {} @Override public void mouseExited(final MouseEvent e) { m_mouseOverPanel = false; } @Override public void mouseEntered(final MouseEvent e) { m_mouseOverPanel = true; } }; addMouseListener(mouseFocusListener); m_data = data; m_details = details; setLayout(new BorderLayout()); if (!m_data.areChangesOnlyInSwingEventThread()) { throw new IllegalStateException(); } m_tree = new JTree(m_data.getHistory()); m_data.getHistory().setTreePanel(this); m_tree.expandRow(0); m_popup = popup; m_tree.add(m_popup); m_popup.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuCanceled(final PopupMenuEvent pme) { m_currentPopupNode = null; } @Override public void popupMenuWillBecomeInvisible(final PopupMenuEvent pme) {} @Override public void popupMenuWillBecomeVisible(final PopupMenuEvent pme) {} }); final HistoryTreeCellRenderer renderer = new HistoryTreeCellRenderer(uiContext); renderer.setLeafIcon(null); renderer.setClosedIcon(null); renderer.setOpenIcon(null); renderer.setBackgroundNonSelectionColor(getBackground()); m_tree.setCellRenderer(renderer); m_tree.setBackground(getBackground()); final JScrollPane scroll = new JScrollPane(m_tree); scroll.addMouseListener(mouseFocusListener); for (final Component comp : scroll.getComponents()) { comp.addMouseListener(mouseFocusListener); } scroll.setBorder(null); scroll.setViewportBorder(null); add(scroll, BorderLayout.CENTER); m_tree.setEditable(false); final HistoryNode node = m_data.getHistory().getLastNode(); m_data.getHistory().gotoNode(node); m_tree.expandPath(new TreePath(node.getPath())); m_tree.setSelectionPath(new TreePath(node.getPath())); m_currentPopupNode = null; final JButton previousButton = new JButton("<-Back"); previousButton.addMouseListener(mouseFocusListener); previousButton.addActionListener(e -> previous()); final JButton nextButton = new JButton("Next->"); nextButton.addMouseListener(mouseFocusListener); nextButton.addActionListener(e -> next()); final JPanel buttons = new JPanel(); buttons.setLayout(new GridBagLayout()); buttons.add(previousButton, new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); buttons.add(nextButton, new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); add(buttons, BorderLayout.SOUTH); m_tree.addMouseListener(new MouseListener() { @Override public void mouseClicked(final MouseEvent me) { if (SwingUtilities.isRightMouseButton(me)) { m_currentPopupNode = (HistoryNode) m_tree.getClosestPathForLocation(me.getX(), me.getY()).getLastPathComponent(); m_popup.show(me.getComponent(), me.getX(), me.getY()); } else if (m_mouseWasOverPanel) { final TreePath clickedPath = new TreePath( ((HistoryNode) m_tree.getClosestPathForLocation(me.getX(), me.getY()).getLastPathComponent()).getPath()); adaptStayExpandedPathsOnClickedPath(clickedPath); } } private void adaptStayExpandedPathsOnClickedPath(final TreePath clickedPath) { if (m_stayExpandedPaths.contains(clickedPath)) { m_stayExpandedPaths.remove(clickedPath); m_tree.collapsePath(clickedPath); } else { m_stayExpandedPaths.add(clickedPath); m_tree.expandPath(clickedPath); } } @Override public void mouseEntered(final MouseEvent me) { m_mouseOverPanel = true; } @Override public void mouseExited(final MouseEvent me) { m_mouseOverPanel = false; } @Override public void mousePressed(final MouseEvent me) {} @Override public void mouseReleased(final MouseEvent me) {} }); m_tree.addTreeSelectionListener(e -> treeSelectionChanged(e)); } private void previous() { if (m_tree.getSelectionCount() == 0) { m_tree.setSelectionInterval(0, 0); return; } final TreePath path = m_tree.getSelectionPath(); final TreeNode selected = (TreeNode) path.getLastPathComponent(); @SuppressWarnings("unchecked") final Enumeration<TreeNode> nodeEnum = ((DefaultMutableTreeNode) m_tree.getModel().getRoot()).depthFirstEnumeration(); TreeNode previous = null; while (nodeEnum.hasMoreElements()) { final TreeNode current = nodeEnum.nextElement(); if (current == selected) { break; } else if (current.getParent() instanceof Step) { previous = current; } } if (previous != null) { navigateTo(previous); } } private void navigateTo(final TreeNode target) { final TreeNode[] nodes = ((DefaultMutableTreeNode) target).getPath(); final TreePath newPath = new TreePath(nodes); m_tree.expandPath(newPath); m_tree.setSelectionPath(newPath); final int row = m_tree.getRowForPath(newPath); if (row == -1) { return; } final Rectangle bounds = m_tree.getRowBounds(row); if (bounds == null) { return; } // scroll to the far left bounds.x = 0; bounds.width = 10; m_tree.scrollRectToVisible(bounds); } private void next() { if (m_tree.getSelectionCount() == 0) { m_tree.setSelectionInterval(0, 0); return; } final TreePath path = m_tree.getSelectionPath(); final TreeNode selected = (TreeNode) path.getLastPathComponent(); @SuppressWarnings("unchecked") final Enumeration<TreeNode> nodeEnum = ((DefaultMutableTreeNode) m_tree.getModel().getRoot()).preorderEnumeration(); TreeNode next = null; boolean foundSelected = false; while (nodeEnum.hasMoreElements()) { final TreeNode current = nodeEnum.nextElement(); if (current == selected) { foundSelected = true; } else if (foundSelected) { if (current.getParent() instanceof Step) { next = current; break; } } } if (next != null) { navigateTo(next); } } private void treeSelectionChanged(final TreeSelectionEvent e) { if (!SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Wrong thread"); } // move the game to the state of the selected node final HistoryNode node = (HistoryNode) e.getPath().getLastPathComponent(); gotoNode(node); } private void gotoNode(final HistoryNode node) { if (!SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Not EDT"); } if (m_details != null) { m_details.render(node); } m_data.getHistory().gotoNode(node); } public HistoryNode getCurrentNode() { final TreePath path = m_tree.getSelectionPath(); final HistoryNode curNode = (HistoryNode) path.getLastPathComponent(); return curNode; } public HistoryNode getCurrentPopupNode() { return m_currentPopupNode; } public void clearCurrentPopupNode() { m_currentPopupNode = null; } // remember which paths were expanded final Collection<TreePath> m_stayExpandedPaths = new ArrayList<>(); private boolean m_mouseOverPanel = false; // to distinguish the first mouse over panel event from the others boolean m_mouseWasOverPanel = false; // remember where to start collapsing TreePath m_lastParent = null; private boolean addToStayExpanded(final Enumeration<TreePath> paths) { final Collection<TreePath> expandPaths = new ArrayList<>(); while (paths.hasMoreElements()) { expandPaths.add(paths.nextElement()); } return m_stayExpandedPaths.addAll(expandPaths); } /** * collapses parents of last path if it is not in the list of expanded path until the new path is a descendant. * * @param newPath * new path */ private void collapseUpFromLastParent(final TreePath newPath) { TreePath currentParent = m_lastParent; while (currentParent != null && !currentParent.isDescendant(newPath) && !stayExpandedContainsDescendantOf(currentParent)) { m_tree.collapsePath(currentParent); currentParent = currentParent.getParentPath(); } } /** * @param parentPath * tree path for which descendants should be check. * @return whether the expanded path list contains a descendant of parentPath */ private boolean stayExpandedContainsDescendantOf(final TreePath parentPath) { for (final TreePath currentPath : m_stayExpandedPaths) { if (parentPath.isDescendant(currentPath)) { return true; } } return false; } /** * collapses expanded paths except if new path is a descendant. * * @param newPath * new path */ private void collapseExpanded(final TreePath newPath) { if (!m_stayExpandedPaths.isEmpty()) { // get enumeration of expanded nodes TreePath root = newPath; while (root.getPathCount() > 1) { root = root.getParentPath(); } final Enumeration<TreePath> expandedDescendants = m_tree.getExpandedDescendants(root); final TreePath selectedPath = m_tree.getSelectionPath(); // fill stack with nodes that should be collapsed final Stack<TreePath> collapsePaths = new Stack<>(); while (expandedDescendants.hasMoreElements()) { final TreePath currentDescendant = expandedDescendants.nextElement(); if (!currentDescendant.isDescendant(newPath) && (selectedPath == null || !currentDescendant.isDescendant(selectedPath))) { collapsePaths.add(currentDescendant); } } // collapse found paths if (!collapsePaths.isEmpty()) { for (final TreePath currentPath : collapsePaths) { m_tree.collapsePath(currentPath); } m_stayExpandedPaths.removeAll(collapsePaths); } } } public void goToEnd() { final HistoryNode last; try { m_data.acquireWriteLock(); last = m_data.getHistory().getLastNode(); } finally { m_data.releaseWriteLock(); } final TreePath path = new TreePath(last.getPath()); final TreePath parent = path.getParentPath(); if (!m_mouseOverPanel) { // make sure we undo our change of the lock property gotoNode(last); if (m_lastParent == null) { m_lastParent = m_tree.getSelectionPath(); } m_tree.setSelectionPath(path); collapseExpanded(path); collapseUpFromLastParent(parent); final Rectangle rect = m_tree.getPathBounds(path); rect.setRect(0, rect.getY(), rect.getWidth(), rect.getHeight()); m_tree.scrollRectToVisible(rect); } else { if (m_mouseWasOverPanel == false) { // save the lock property so that we can undo it TreePath root = parent; while (root.getPathCount() > 1) { root = root.getParentPath(); } final Enumeration<TreePath> expandedDescendants = m_tree.getExpandedDescendants(root); addToStayExpanded(expandedDescendants); } else { collapseUpFromLastParent(parent); } m_tree.expandPath(parent); } m_mouseWasOverPanel = m_mouseOverPanel; m_lastParent = parent; } } class HistoryTreeCellRenderer extends DefaultTreeCellRenderer { private static final long serialVersionUID = -72258573320689596L; private final ImageIcon icon = new ImageIcon(); private final IUIContext m_uiContext; public HistoryTreeCellRenderer(final IUIContext uiContext) { m_uiContext = uiContext; } @Override public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean haveFocus) { if (value instanceof Step) { final PlayerID player = ((Step) value).getPlayerID(); if (player != null) { if (m_uiContext != null) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, haveFocus); icon.setImage(m_uiContext.getFlagImageFactory().getSmallFlag(player)); setIcon(icon); } else { final String text = value.toString() + " (" + player.getName() + ")"; super.getTreeCellRendererComponent(tree, text, sel, expanded, leaf, row, haveFocus); } } else { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, haveFocus); } } else { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, haveFocus); } return this; } }