/* * Copyright (C) 2013 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.openmicroscopy.shoola.keywords; import java.awt.Component; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JTree; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import abbot.finder.BasicFinder; import abbot.finder.ComponentNotFoundException; import abbot.finder.Matcher; import abbot.finder.MultipleComponentsFoundException; import abbot.tester.JTreeTester; import abbot.util.AWT; import org.netbeans.jemmy.Waitable; import org.netbeans.jemmy.Waiter; import org.netbeans.jemmy.operators.JTreeOperator; import org.robotframework.swing.common.TimeoutCopier; import org.robotframework.swing.common.TimeoutName; import org.robotframework.swing.tree.NodeTextExtractor; import org.robotframework.swing.tree.TreeOperator; import com.google.common.base.Joiner; import com.google.common.base.Strings; /** * Robot Framework SwingLibrary keyword library offering methods for working with the tree view. * @author m.t.b.carroll@dundee.ac.uk * @since 4.4.9 */ public class JTreeLibrary { /** Allow Robot Framework to instantiate this library only once. */ public static final String ROBOT_LIBRARY_SCOPE = "GLOBAL"; /** * Gets tree paths in a form suitable for SwingLibrary. * @author m.t.b.carroll@dundee.ac.uk * @since 4.4.9 */ private static class TreePathGetter { private final NodeTextExtractor treeNodeTextExtractor; /** * Construct a tree path getter for the given tree. * @param tree a tree */ TreePathGetter(JTree tree) { this.treeNodeTextExtractor = new NodeTextExtractor(tree); } /** * Get the tree node path in a form suitable for SwingLibrary. * @param path the Swing tree node path * @return the specifier for the path */ public String getTreeNodePath(TreePath path) { final int pathLength = path.getPathCount(); final List<String> pathText = new ArrayList<String>(pathLength); for (int nodeIndex = 0; nodeIndex < pathLength; nodeIndex++) { final Object node = path.getPathComponent(nodeIndex); final String nodeText = this.treeNodeTextExtractor.getText(node, path); if (!Strings.isNullOrEmpty(nodeText)) { pathText.add(nodeText); } } return Joiner.on('|').join(pathText); } } /** * A matcher for tree nodes. * @author m.t.b.carroll@dundee.ac.uk * @since 4.4.9 */ private interface TreeNodeMatcher { /** * Check if the sought tree node is the given one. * @param treePath the tree path to the node * @param component the component used in rendering the tree cell * @param node the tree node * @return if the tree node matches */ public boolean matches(TreePath treePath, Component component, Object node); } /** * Wraps the <code>Get Matching Tree Path</code> keyword for Robot Framework * in a Jemmy {@link org.robotframework.org.netbeans.jemmy.Waiter}. * @author m.t.b.carroll@dundee.ac.uk * @since 4.4.9 */ private static class GetMatchingTreePathWaiter implements Waitable { private final JTree tree; private final Pattern pattern; private final TreePathGetter treePathGetter; /** * Construct a new waiter with the <code>Get Matching Tree Path</code> arguments. * Note that the <code>JTree</code> is assumed to already be available even if a matching node is not. * @param tree the <code>JTree</code> instance in which to search for the tree path * @param pattern the <code>Pattern</code> that must match the whole tree path */ GetMatchingTreePathWaiter(JTree tree, Pattern pattern) { this.tree = tree; this.pattern = pattern; this.treePathGetter = new TreePathGetter(tree); } /** * @return a matching tree path if any now exist, or <code>null</code> if none do */ public String getMatchingTreePath() { /* check the JTree's model */ final TreeModel genericModel = this.tree.getModel(); if (!(genericModel instanceof DefaultTreeModel)) { return null; } final DefaultTreeModel model = (DefaultTreeModel) genericModel; /* iterate through the tree paths */ final Set<List<Object>> treePaths = new HashSet<List<Object>>(); treePaths.add(Collections.singletonList(model.getRoot())); while (!treePaths.isEmpty()) { /* consider the next tree path */ final Iterator<List<Object>> treePathIterator = treePaths.iterator(); final List<Object> nextTreePath = treePathIterator.next(); treePathIterator.remove(); /* check if the tree path string matches the regular expression */ final String treePathString = this.treePathGetter.getTreeNodePath(new TreePath(nextTreePath.toArray())); if (this.pattern.matcher(treePathString).matches()) { return treePathString; } /* if not, then note to check the path's children */ final Object nextNode = nextTreePath.get(nextTreePath.size() - 1); int nextChild = model.getChildCount(nextNode); while (nextChild-- > 0) { final Object child = model.getChild(nextNode, nextChild); final List<Object> childTreePath = new ArrayList<Object>(nextTreePath.size() + 1); childTreePath.addAll(nextTreePath); childTreePath.add(child); treePaths.add(childTreePath); } } return null; } /** * {@inheritDoc} */ public Object actionProduced(Object ignored) { return getMatchingTreePath(); } /** * {@inheritDoc} */ public String getDescription() { return "find a path in the tree named " + this.tree.getName() + " matching " + this.pattern.pattern(); } } /** * <table> * <td>Get Matching Tree Path</td> * <td>regular expression</td> * <td><code>JTree</code> component name</td> * <table> * @param regExp a regular expression against which to match whole paths, cf. {@link java.util.regex.Pattern} * @param treeName the name of the tree whose paths are to be searched * @return a tree path from the tree that matches the regular expression * @throws InterruptedException if the matcher was interrupted while waiting for a suitable tree path * @throws MultipleComponentsFoundException if multiple suitable components have the given name * @throws ComponentNotFoundException if no suitable components have the given name */ public String getMatchingTreePath(final String regExp, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException, InterruptedException { final JTree tree = (JTree) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JTree && treeName.equals(component.getName()); }}); final Waiter waiter = new Waiter(new GetMatchingTreePathWaiter(tree, Pattern.compile(regExp))); waiter.setTimeouts(new TimeoutCopier(new JTreeOperator(tree), TimeoutName.J_TREE_OPERATOR_WAIT_NODE_VISIBLE_TIMEOUT).getTimeouts()); return (String) waiter.waitAction(null); } /** * <table> * <td>Get Selected Tree Path</td> * <td><code>JTree</code> component name</td> * <table> * @param treeName the name of the tree whose node selection is queried * @return a tree path from the tree that is the first selected node, or <code>null</code> if none are selected * @throws MultipleComponentsFoundException if multiple suitable components have the given name * @throws ComponentNotFoundException if no suitable components have the given name */ public String getSelectedTreePath(final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { final JTree tree = (JTree) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JTree && treeName.equals(component.getName()); }}); final TreePath path = tree.getSelectionPath(); return path == null ? null : new TreePathGetter(tree).getTreeNodePath(path); } /** * Find the path to a specific node in a tree, expanding nodes during the hunt. * @param matcher to identify the sought node * @param treeName the name of the tree among whose nodes to search * @return the path to a matching tree node * @throws MultipleComponentsFoundException if multiple suitable trees have the given name * @throws ComponentNotFoundException if no suitable trees nodes have the given name or no nodes match */ public String findInTree(TreeNodeMatcher matcher, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { final JTree tree = (JTree) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JTree && treeName.equals(component.getName()); }}); return findInTree(matcher, tree); } /** * Find the path to a specific node in a tree, expanding nodes during the hunt. * @param matcher to identify the sought node * @param tree the tree among whose nodes to search * @return the path to a matching tree node * @throws ComponentNotFoundException if no nodes match */ private String findInTree(TreeNodeMatcher matcher, final JTree tree) throws ComponentNotFoundException { final Set<TreePath> wrongTreePaths = new HashSet<TreePath>(); final Set<TreePath> pathsToExpand = new HashSet<TreePath>(); final TreeModel model = tree.getModel(); final TreeCellRenderer renderer = tree.getCellRenderer(); while (true) { final int rowCount = tree.getRowCount(); for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { final TreePath path = tree.getPathForRow(rowIndex); if (path == null || wrongTreePaths.contains(path)) { continue; } final Object node = path.getLastPathComponent(); final Component rowComponent = renderer.getTreeCellRendererComponent(tree, node, false, false, false, rowIndex, false); if (matcher.matches(path, rowComponent, node)) { tree.setSelectionRow(rowIndex); return new TreePathGetter(tree).getTreeNodePath(path); } if (!(pathsToExpand.contains(path) || model.isLeaf(path.getLastPathComponent()) || tree.isExpanded(rowIndex))) { pathsToExpand.add(path); } wrongTreePaths.add(path); } final Iterator<TreePath> pathIterator = pathsToExpand.iterator(); if (pathIterator.hasNext()) { tree.expandPath(pathIterator.next()); pathIterator.remove(); try { /* TODO: perhaps some specific component state can be awaited, applying Jemmy timeouts */ Thread.sleep(2000); } catch (InterruptedException e) { } } else { throw new ComponentNotFoundException(); } } } /** * <table> * <td>Get Tree Path With Image Icon</td> * <td>name of the sought path's icon</td> * <td><code>JTree</code> component name</td> * <table> * @param iconName the name of the icon sought among the tree nodes * @param treeName the name of the tree among whose nodes to search * @return a tree path from the tree whose node bears the given icon * @throws MultipleComponentsFoundException if multiple suitable components have the given name * @throws ComponentNotFoundException if no suitable components have the given name */ public String getTreePathWithImageIcon(final String iconName, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { final TreeNodeMatcher matcher = new TreeNodeMatcher() { @Override public boolean matches(TreePath treepath, Component component, Object node) { return iconName.equals(IconCheckLibrary.getIconNameMaybe(component)); }}; return findInTree(matcher, treeName); } /** * <table> * <td>Get Tree Path With Icon And Name</td> * <td>name of the sought node's image icon</td> * <td>name of the sought node</td> * <td><code>JTree</code> component name</td> * <table> * @param iconName the name of the icon sought among the tree nodes * @param nodeName the name of the sought node * @param treeName the name of the tree among whose nodes to search * @return a tree path from the tree whose node matches the given criteria * @throws MultipleComponentsFoundException if multiple suitable components have the given name * @throws ComponentNotFoundException if no suitable components have the given name or no suitable node can be found */ public String getTreePathWithIconAndName(final String iconName, final String nodeName, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { final JTree tree = (JTree) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JTree && treeName.equals(component.getName()); }}); final NodeTextExtractor treeNodeTextExtractor = new NodeTextExtractor(tree); final TreeNodeMatcher matcher = new TreeNodeMatcher() { @Override public boolean matches(TreePath treePath, Component component, Object node) { return nodeName.equals(treeNodeTextExtractor.getText(node, treePath)) && iconName.equals(IconCheckLibrary.getIconNameMaybe(component)); }}; return findInTree(matcher, tree); } /** * Test if the tree node popup menu item is enabled. * (For motivation, see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/11326">trac #11326</a>.) * This initial version detects only first-level menu items, not paths to submenus. * @param menuItemPath the path of the menu item * @param treePath the path to the node whose popup is to be queried * @param treeName the name of the tree that has the node of interest * @return if the specified menu item is enabled * @throws MultipleComponentsFoundException if multiple suitable components could be found * @throws ComponentNotFoundException if no suitable components could be found */ private boolean isTreeNodeMenuItemEnabled(final String menuItemPath, final String treePath, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { final JTree tree = (JTree) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JTree && treeName.equals(component.getName()); }}); final JTreeOperator operator = new JTreeOperator(tree); operator.clickOnPath(new TreeOperator(operator).findPath(treePath), 1, AWT.getPopupMask()); final JPopupMenu menu = (JPopupMenu) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { return component instanceof JPopupMenu && ((JPopupMenu) component).getInvoker() == tree; }}); final JMenuItem menuItem = (JMenuItem) new BasicFinder().find(new Matcher() { public boolean matches(Component component) { final List<String> menuItemText = new ArrayList<String>(); while (component instanceof JMenuItem) { menuItemText.add(0, ((JMenuItem) component).getText()); component = component.getParent(); if (component instanceof JPopupMenu) { final Component invoker = ((JPopupMenu) component).getInvoker(); if (invoker instanceof JMenuItem) { component = invoker; } } } return component == menu && Joiner.on('|').join(menuItemText).equals(menuItemPath); }}); final boolean isEnabled = menuItem.isEnabled(); new JTreeTester().actionKeyStroke(KeyEvent.VK_ESCAPE); /* to close the popup menu */ return isEnabled; } /** * <table> * <td>Tree Node Menu Item Should Be Enabled</td> * <td>the text of the menu item</td> * <td>the path to the tree node</td> * <td><code>JTree</code> component name</td> * </table> * @param menuItemPath the path of the menu item * @param treePath the path to the node whose popup is to be queried * @param treeName the name of the tree that has the node of interest * @throws MultipleComponentsFoundException if multiple suitable components could be found * @throws ComponentNotFoundException if no suitable components could be found */ public void treeNodeMenuItemShouldBeEnabled(final String menuItemPath, final String treePath, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { if (!isTreeNodeMenuItemEnabled(menuItemPath, treePath, treeName)) { throw new RuntimeException("Menu item '" + menuItemPath + "' was disabled"); } } /** * <table> * <td>Tree Node Menu Item Should Be Disabled</td> * <td>the text of the menu item</td> * <td>the path to the tree node</td> * <td><code>JTree</code> component name</td> * </table> * @param menuItemPath the path of the menu item * @param treePath the path to the node whose popup is to be queried * @param treeName the name of the tree that has the node of interest * @throws MultipleComponentsFoundException if multiple suitable components could be found * @throws ComponentNotFoundException if no suitable components could be found */ public void treeNodeMenuItemShouldBeDisabled(final String menuItemPath, final String treePath, final String treeName) throws ComponentNotFoundException, MultipleComponentsFoundException { if (isTreeNodeMenuItemEnabled(menuItemPath, treePath, treeName)) { throw new RuntimeException("Menu item '" + menuItemPath + "' was enabled"); } } }