package abbot.tester; import java.awt.*; import java.awt.event.KeyEvent; import java.util.Arrays; import javax.accessibility.AccessibleContext; import javax.swing.*; import abbot.*; import abbot.i18n.Strings; import abbot.util.AWT; /** Provides auto-scrolling prior to events for JComponent-derived classes. */ // NOTE may eventually need to push ComponentLocation up to Robot, so that // only mousePress and actionDrop need to be overridden. This would be mostly // aesthetic, since making the target point visible is sufficient; having the // entire substructure visible is just more pleasing to the eye. // FIXME may need to override key/focus actions to scroll prior to sending the // events, similar to how click is overridden here public class JComponentTester extends ContainerTester { /** This property is a duplicate of the one in JLabel, which we can't * access. */ private static final String LABELED_BY_PROPERTY = "labeledBy"; /** Derive a tag for identifying this component. */ public String deriveTag(Component comp) { // If the component class is custom, don't provide a tag if (isCustom(comp.getClass())) return null; JComponent jComp = ((JComponent)comp); String tag = null; // If label.setLabelFor has been used, then this component has // a label; use its text JLabel label = (JLabel) ((JComponent)comp).getClientProperty(LABELED_BY_PROPERTY); if (label != null && label.getText() != null && label.getText().length() > 0) { tag = label.getText(); } if (tag == null || "".equals(tag)) { AccessibleContext context = jComp.getAccessibleContext(); tag = deriveAccessibleTag(context); } if (tag == null || "".equals(tag)) { tag = super.deriveTag(comp); } return tag; } /** Scrolls to ensure the substructure is in view before clicking. @deprecated Use {@link #actionClick(Component, ComponentLocation, int, int)} instead. */ public void actionClick(Component c, ComponentLocation loc, String buttons, int count) { actionClick(c, loc, AWT.getModifiers(buttons), count); } /** Scrolls to ensure the substructure is in view before clicking. */ public void actionClick(Component c, ComponentLocation loc, int buttons, int count) { if (c instanceof JComponent) { scrollToVisible(c, loc.getBounds(c)); } super.actionClick(c, loc, buttons, count); } /** @deprecated Use {@link #actionDrag(Component, ComponentLocation, int)} instead. */ public void actionDrag(Component c, ComponentLocation loc, String mods) { actionDrag(c, loc, AWT.getModifiers(mods)); } /** Scrolls to ensure the substructure is in view before starting the * drag. */ public void actionDrag(Component c, ComponentLocation loc, int modifiers) { if (c instanceof JComponent) { scrollToVisible(c, loc.getBounds(c)); } super.actionDrag(c, loc, modifiers); } /** Scrolls to ensure the drop target substructure is in view before dropping (normally handled by autoscroll). */ public void actionDrop(Component c, ComponentLocation loc) { if (c instanceof JComponent) { scrollToVisible(c, loc.getBounds(c)); } super.actionDrop(c, loc); } /** Click in the given part of the component, scrolling the component if * necessary to make the point visible. Performing the scroll here * obviates the need for all derived classes to remember to do it for * actions involving clicks. */ public void mousePress(Component comp, int x, int y, int buttons) { if (comp instanceof JComponent) { scrollToVisible(comp, x, y); } super.mousePress(comp, x, y, buttons); } /** * Scrolls the component so that the coordinate x and y are visible. Has * no effect if the component has no JViewport ancestor. * * @param comp the Component to scroll * @param x the x coordinate to be visible * @param y the y coordinate to be visible */ protected void scrollToVisible(Component comp, int x, int y) { Rectangle rect = new Rectangle(x, y, 1, 1); scrollToVisible(comp, rect); } /** Invoke {@link JComponent#scrollRectToVisible(Rectangle)} on the given {@link JComponent} on the event dispatch thread. */ protected void scrollRectToVisible(final JComponent jc, final Rectangle rect) { // Ideally, we'd use scrollbar commands to effect the scrolling, // but that gets really complicated for no real gain in function. // Fortunately, Swing's Scrollable makes for a simple solution. // NOTE: absolutely MUST wait for idle in order for the scroll to // finish, and the UI to update so that the next action goes // to the proper location within the scrolled component. invokeAndWait(new Runnable() { public void run() { jc.scrollRectToVisible(rect); } }); } protected boolean isVisible(JComponent c, Rectangle rect) { Rectangle visible = c.getVisibleRect(); return visible.contains(rect); } protected boolean isVisible(JComponent c, int x, int y) { Rectangle visible = c.getVisibleRect(); return visible.contains(x, y); } /** * Scrolls the component so that the given rectangle is visible. Has no * effect if the component has no JViewport ancestor. When this method * returns, the requested rectangle's upper left corner will be visible * (i.e. no {@link #waitForIdle} is required. * * @param comp the Component to scroll * @param rect the Rectangle to make visible. */ protected void scrollToVisible(Component comp, final Rectangle rect) { final JComponent jc = (JComponent)comp; if (!isVisible(jc, rect)) { scrollRectToVisible(jc, rect); // Need to make at least the upper left corner of the requested // rectangle visible. if (!isVisible(jc, rect.x, rect.y)) { String msg = Strings.get("tester.JComponent.not_visible", new Object[] { new Integer(rect.x), new Integer(rect.y), jc, }); throw new ActionFailedException(msg); } } } /** Make sure the given point is visible. Note that this may have no * effect if the component is not actually in a scroll pane. */ public void actionScrollToVisible(Component comp, ComponentLocation loc) { scrollToVisible(comp, loc.getBounds(comp)); waitForIdle(); } /** Make sure the given point is visible. Note that this may have no * effect if the component is not actually in a scroll pane. */ public void actionScrollToVisible(Component comp, int x, int y) { actionScrollToVisible(comp, new ComponentLocation(new Point(x, y))); } /** Make sure the given rectangle is visible. Note that this may have no * effect if the component is not actually in a scroll pane. */ public void actionScrollToVisible(Component comp, int x, int y, int width, int height) { scrollToVisible(comp, new Rectangle(x, y, width, height)); waitForIdle(); } /** Invoke an action from the component's action map. */ public void actionActionMap(Component comp, String name) { focus(comp, true); JComponent jc = (JComponent)comp; ActionMap am = jc.getActionMap(); // On OSX/1.3.1, some action map keys are actions instead of strings. // On XP/1.4.1, all action map keys are strings. // If we can't look it up with the string key we saved, check all the // actions for a corresponding name. Object action = am.get(name); if (action == null) { Object[] keys = am.allKeys(); for (int i=0;keys != null && i < keys.length;i++) { Object value = am.get(keys[i]); if ((value instanceof Action)) { String aname = (String) ((Action)value).getValue(Action.NAME); if (aname != null && aname.equals(name)) { action = value; break; } } } } if (action == null) { String available = "Available actions are the following:"; Object[] names = am.allKeys(); if (names != null) { Arrays.sort(names, new java.util.Comparator() { public int compare(Object o1, Object o2) { String n1 = o1.toString(); String n2 = o2.toString(); return n1.compareTo(n2); } }); for (int i=0;i < names.length;i++) { available += "\n" + names[i]; if (!(names[i] instanceof String)) available += " (" + names[i].getClass() + ")"; } } throw new AssertionFailedError("No such action '" + name + "'. " + available); } InputMap im = jc.getInputMap(); KeyStroke[] events = im.allKeys(); for (int i=0;events != null && i < events.length;i++) { KeyStroke ks = events[i]; Object key = im.get(ks); // If the key is an action (OSX/1.3.1), grab the action name // instead Log.debug("ks=" + ks + " key=" + key); if (key instanceof Action) { Object nm = ((Action)key).getValue(Action.NAME); if (nm != null) key = nm; } if (name.equals(key)) { Log.debug("Generating keystroke " + ks + " for action " + name); if (ks.getKeyCode() == KeyEvent.VK_UNDEFINED) keyStroke(ks.getKeyChar()); else key(ks.getKeyCode(), ks.getModifiers()); waitForIdle(); return; } } throw new ActionFailedException("No input event found for action key '" + name + "'"); } /** Return a shared instance of JComponentTester. */ public static JComponentTester getTester(JComponent c) { return (JComponentTester)ComponentTester.getTester(JComponent.class); } }