Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-6910471

vetoing permanentFocusOwner properties can cause an infinite loop

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: P4 P4
    • None
    • 6u10
    • client-libs
    • x86
    • linux

      FULL PRODUCT VERSION :
      java version "1.6.0_10"
      Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
      Java HotSpot(TM) Server VM (build 11.0-b15, mixed mode)


      ADDITIONAL OS VERSION INFORMATION :
      Linux linux-dev20 2.6.18-164.6.1.el5 #1 SMP Tue Nov 3 16:18:27 EST 2009 i686 i686 i386 GNU/Linux


      A DESCRIPTION OF THE PROBLEM :
      Note: this bug seems similar to bug-id#4504665, which marked as fix delivered, verified back in 2002.

      I have added a VetoableChangeListener to the default focus manager and set it to conditionally veto focus changes away from one of the components in the attached demo application. The following log (produced from the program but with backtraces removed) shows the following information on each call to "vetoableChange":

      (1) Information about the PropertyChangeEvent.
      (2) The current permanent focus owner.
      (3) The AWTEvent that is currently being processed (EventQueue.getCurrentEvent()).
      (4) The next event on the event queue.

      The problem occurs in the fifth call to "vetoableChange". It looks like the focus manager has synthesized "FOCUS_LOST/FOCUS_GAINED" events transferring focus from button "A" back to button "C", but that the PropertyChangeEvent calls show a transfer from "C" to "C". This is modeled as a change from "C" to null, then from "null" to "C". Unfortunately, there is no way for the vetoableChange code to distinguish the fifth call from the first one without using EventQueue.getCurrentEvent() to examine the underlying AWT event.

      Examining the event queue may be an acceptable workaround, but it is not mentioned in the documentation (as far as I can see, anyway), and it's not clear that the API guarantees that the appropriate FocusEvent will actually be on the queue.

      As a fix, the system could (1) suppress the "ROLLBACK" calls (since the focus was never actually transferred), (2) suppress the "vetoableChange" call from "C" to "C", (3) make the "old" value in the "vetoableChange" call correspond to the target of the "FOCUS_LOST" event, or (4) provide a documented mechanism to disambiguate the two cases.

      If one attempts to veto the fifth call, then the system goes into an infinite loop. Also, one uses "focusOwner" instead of "permanentFocusOwner" then the system seems to go into an infinite loop whether the fifth call is vetoed or not.

      #
      # First call, caused by FOCUS_LOST, vetoed.
      #
      vetoableChange: name=permanentFocusOwner, old=[TestButton: C], new=null, propId=null
          permanent focus owner=[TestButton: C]
          underlying event=java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: A],cause=TRAVERSAL_FORWARD] on [TestButton: C]
          peek event=java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: C],cause=TRAVERSAL_FORWARD] on [TestButton: A]
      java.beans.PropertyVetoException: vetoing change from component 3

      #
      # Second call... aparently undoing the first change.
      #
      vetoableChange: name=permanentFocusOwner, old=null, new=[TestButton: C], propId=null
          permanent focus owner=[TestButton: C]
          underlying event=java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: A],cause=TRAVERSAL_FORWARD] on [TestButton: C]
          peek event=java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: C],cause=TRAVERSAL_FORWARD] on [TestButton: A]

      #
      # Third call, caused by FOCUS_GAINED, vetoed.
      #
      vetoableChange: name=permanentFocusOwner, old=[TestButton: C], new=[TestButton: A], propId=null
          permanent focus owner=[TestButton: C]
          underlying event=java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: C],cause=TRAVERSAL_FORWARD] on [TestButton: A]
          peek event=java.awt.event.InvocationEvent[INVOCATION_DEFAULT,runnable=java.awt.KeyboardFocusManager$1@da4b71,notifier=null,catchExceptions=false,when=1259773927573] on sun.awt.X11.XToolkit@ff057f
      java.beans.PropertyVetoException: vetoing change from component 3

      #
      # Fourth call... aparently undoing the third change.
      #
      vetoableChange: name=permanentFocusOwner, old=[TestButton: A], new=[TestButton: C], propId=null
          permanent focus owner=[TestButton: C]
          underlying event=java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: C],cause=TRAVERSAL_FORWARD] on [TestButton: A]
          peek event=java.awt.event.InvocationEvent[INVOCATION_DEFAULT,runnable=java.awt.KeyboardFocusManager$1@da4b71,notifier=null,catchExceptions=false,when=1259773927573] on sun.awt.X11.XToolkit@ff057f

      #
      # Fifth call... caused by a new FOCUS_LOST event as part of a ROLLBACK. Note that
      # although the FOCUS_LOST event indicates that [TestButton: A] is losing focus to [TestButton: C],
      # that the PropertyChangeEvent reports that the focus is being taken from [TestButton: C]!
      #
      vetoableChange: name=permanentFocusOwner, old=[TestButton: C], new=null, propId=null
          permanent focus owner=[TestButton: C]
          underlying event=java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: C],cause=ROLLBACK] on [TestButton: A]
          peek event=java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: C],cause=ROLLBACK] on [TestButton: A]
      focusLost: Ajava.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: C],cause=ROLLBACK] on [TestButton: A]

      #
      # Sixth call... caused by a new FOCUS_GAINED event. This time the new is correctly reported as being [TestButton:C].
      #
      vetoableChange: name=permanentFocusOwner, old=null, new=[TestButton: C], propId=null
          permanent focus owner=null
          underlying event=java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: A],cause=ROLLBACK] on [TestButton: C]
          peek event=java.awt.FocusEvent[FOCUS_LOST,permanent,opposite=[TestButton: C],cause=ROLLBACK] on [TestButton: A]
      focusGained: C: java.awt.FocusEvent[FOCUS_GAINED,permanent,opposite=[TestButton: A],cause=ROLLBACK] on [TestButton: C]

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached program. Select the "Flag" and "Works?" buttons, then select "C". At this point button "C" will be green, "Flag" will be red, and "Works?" will be green. Now press the TAB key to transfer focus back to button "A". The focus will not transfer, and the log will be very similar to the log annotated in the description.

      Now, press the "Works?" button again. This switches to a procedure that does not check the underling FocusEvent. Press TAB to attempt a focus transfer to "A". The system will go into an infinite loop.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      A sequence of calls to 'vetoableChange' that provided enough information for the vetoable change listener to decide whether or not a PropertyVetoException should be thrown.
      ACTUAL -
      Insufficient information to decide whether or not to throw PropertyVetoException.

      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      See the description.

      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      package focus;

      import java.awt.Color;
      import java.awt.Component;
      import java.awt.Container;
      import java.awt.EventQueue;
      import java.awt.FontMetrics;
      import java.awt.Frame;
      import java.awt.Graphics;
      import java.awt.KeyboardFocusManager;
      import java.awt.Toolkit;
      import java.awt.event.FocusAdapter;
      import java.awt.event.FocusEvent;
      import java.awt.event.KeyAdapter;
      import java.awt.event.KeyEvent;
      import java.awt.event.MouseAdapter;
      import java.awt.event.MouseEvent;
      import java.awt.event.WindowAdapter;
      import java.awt.event.WindowEvent;
      import java.beans.PropertyChangeEvent;
      import java.beans.PropertyVetoException;
      import java.beans.VetoableChangeListener;

      @SuppressWarnings("serial")
      public class VetoTest extends Frame {

      Container panel;
      Component component1;
      Component component2;
      Component component3;
      boolean isFlagged = false;
      boolean isWorkaround = false;
      Component flagComponent;
      Component workaroundComponent;

      public static void main(String[] args) {
      VetoTest test = new VetoTest();
      test.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent e) {
      System.exit(0);
      }
      });

      test.setVisible(true);
      test.validate();
      }

      public VetoTest() {
      super("Test Focus");

      setLayout(null);
      setBackground(Color.PINK);

      panel = new TestPanel();
      panel.setLayout(null);

      component1 = new TestButton("A", true);
      panel.add(component1);

      component2 = new TestButton("B", true);
      panel.add(component2);

      component3 = new TestButton("C", true);
      panel.add(component3);

      flagComponent = new TestButton("Flag", false) {
      @Override
      protected Color getButtonColor() {
      return isFlagged ? Color.RED : Color.LIGHT_GRAY;
      }

      @Override
      public void setPressed(boolean isPressed) {
      if (isPressed) {
      isFlagged = !isFlagged;
      }
      super.setPressed(isPressed);
      }
      };
      panel.add(flagComponent);

      workaroundComponent = new TestButton("Works?", false) {
      @Override
      protected Color getButtonColor() {
      return isWorkaround ? Color.GREEN : Color.LIGHT_GRAY;
      }

      @Override
      public void setPressed(boolean isPressed) {
      if (isPressed) {
      isWorkaround = !isWorkaround;
      }
      super.setPressed(isPressed);
      }
      };
      panel.add(workaroundComponent);

      add(panel);

      panel.setBounds(0, 0, panel.getComponentCount() * 75 + 25, 100);
      component1.setBounds(25, 25, 50, 50);
      component2.setBounds(100, 25, 50, 50);
      component3.setBounds(175, 25, 50, 50);
      flagComponent.setBounds(250, 25, 50, 50);
      workaroundComponent.setBounds(325, 25, 50, 50);

      final KeyboardFocusManager focusManager = KeyboardFocusManager
      .getCurrentKeyboardFocusManager();

      focusManager.addVetoableChangeListener("permanentFocusOwner",
      new VetoableChangeListener() {

      @Override
      public void vetoableChange(PropertyChangeEvent evt)
      throws PropertyVetoException {
      System.out.println();
      System.out.println("vetoableChange: name="
      + evt.getPropertyName() + ", old="
      + evt.getOldValue() + ", new="
      + evt.getNewValue() + ", propId="
      + evt.getPropagationId());
      System.out.println(" permanent focus owner="
      + focusManager.getPermanentFocusOwner());
      System.out.println(" underlying event="
      + EventQueue.getCurrentEvent());
      EventQueue queue = Toolkit.getDefaultToolkit()
      .getSystemEventQueue();
      System.out.println(" peek event="
      + queue.peekEvent());

      if (!isWorkaround) {
      if (isFlagged && (evt.getOldValue() == component3)
      && (evt.getNewValue() != component3)) {
      PropertyVetoException e = new PropertyVetoException(
      " vetoing change from component 3",
      evt);
      e.printStackTrace(System.out);
      throw e;
      }
      } else {
      /* works on Sun */
      FocusEvent focusEvent = (FocusEvent) EventQueue
      .getCurrentEvent();
      Component target = (focusEvent.getID() == FocusEvent.FOCUS_GAINED ? focusEvent
      .getComponent()
      : (focusEvent.getID() == FocusEvent.FOCUS_LOST ? focusEvent
      .getOppositeComponent()
      : null));

      if (isFlagged && (evt.getOldValue() == component3)
      && (evt.getNewValue() != component3)
      && (target != component3)) {
      PropertyVetoException e = new PropertyVetoException(
      " vetoing change from component 3",
      evt);
      e.printStackTrace(System.out);
      throw e;
      }
      }
      }
      });

      setSize(panel.getSize());
      }

      @Override
      public String toString() {
      return "[Frame: " + getName() + "]";
      }

      /**
      * A light weight "button" that can take focus.
      */
      @SuppressWarnings("serial")
      class TestButton extends Component {
      private boolean isPressed = false;
      private String label;

      public TestButton(String labelText, final boolean isFocusable) {
      this.label = labelText;
      setFocusable(isFocusable);
      addMouseListener(new MouseAdapter() {

      @Override
      public void mousePressed(MouseEvent e) {
      System.out.println("MousePressed: " + label + ": " + e);
      setPressed(true);
      if (isFocusable) {
      requestFocusInWindow();
      }
      }

      @Override
      public void mouseReleased(MouseEvent e) {
      setPressed(false);
      }
      });
      if (isFocusable) {

      addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
      System.out.println("KeyPressed: " + label + ": " + e);
      }
      });

      addFocusListener(new FocusAdapter() {

      @Override
      public void focusGained(FocusEvent e) {
      System.out.println("focusGained: " + label + ": " + e);
      repaint();
      }

      @Override
      public void focusLost(FocusEvent e) {
      System.out.println("focusLost: " + label + e);
      repaint();
      }

      });
      }
      }

      @Override
      public void paint(Graphics g) {
      Color color = getButtonColor();

      g.setColor(color);
      g.fill3DRect(0, 0, getWidth(), getHeight(), !isPressed());

      g.setColor(Color.BLACK);
      FontMetrics metrics = g.getFontMetrics();
      int labelWidth = metrics.stringWidth(label);
      int labelHeight = metrics.getHeight();
      g.drawString(label, (getWidth() - labelWidth) / 2,
      (getHeight() - labelHeight) / 2 + metrics.getAscent());
      }

      protected Color getButtonColor() {
      Color color = hasFocus() ? Color.GREEN.brighter()
      : Color.LIGHT_GRAY;
      return color;
      }

      public boolean isPressed() {
      return isPressed;
      }

      public void setPressed(boolean isPressed) {
      this.isPressed = isPressed;
      repaint();
      }

      @Override
      public String toString() {
      return "[TestButton: " + label + "]";
      }

      }

      /**
      * A simple panel.
      */
      class TestPanel extends Container {
      @Override
      public String toString() {
      return "[TestPanel]";
      }
      }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      One can use "EventQueue.getCurrentEvent()" to find the underlying FocusEvent and then chose whether or not to veto the PropertyChangeEvent based on both the underlying focus even and the property event.

      See the description for details.

            ant Anton Tarasov (Inactive)
            ndcosta Nelson Dcosta (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Imported:
              Indexed: