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

Changing current KeyboardFocusManager -> window activated after modal dismissal

    Details

    • Subcomponent:
    • CPU:
      x86_64
    • OS:
      windows_10

      Description

      ADDITIONAL SYSTEM INFORMATION :
      Via code inspection of WWindowPeer.java, I expect this bug occurs in all versions of Java. Certainly in 1.8.0_211 and above.

      A DESCRIPTION OF THE PROBLEM :
      On windows, calling KeyboardFocusManager.setCurrentKeyboardFocusManager(...) after a WWindowPeer has been initialized can cause an incorrect window to become activated when a modal dialog is closed.

      Modal dialogs being closed will trigger native code `AwtDialog::ModalActivateNextWindow`, which in turn calls into java function `WWindowPeer.getActiveWindowHandles` to find the correct window to activate. That function reads data that is stored in the AppContext. The data is updated by PropertyChangeListener WWindowPeer.ActiveWindowListener. However, that PCL is installed only to the KeyboardFocusManager.getCurrentFocusManager() when the first WWindowPeer is created inside a given AppContext. So if KeyboardFocusManager.setCurrentFocusManager(...) is called after a WWindowPeer is changed, then `WWindowPeer.getActiveWindowHandles` will report stale data, and all modal dialogs will activate windows based upon stale information.

      A possible fix for this issue is to also install a PropertyChangeListener on the KeyboardFocusManager for key "managingFocus", and to move the WWindowPeer.ActiveWindowListener to the new KFM if a PropertyChangeEvent is fired with getOldValue() == Boolean.FALSE && getNewValue() == Boolean.TRUE.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Show a JFrame (or any other component which will trigger `WWindowPeer.initActiveWindowsTracking`)
      2. Call KeyboardFocusManager.setCurrentKeyboardFocusManager(...) with any new KeyboardFocusManager
      3. Show a new JFrame / Window / Dialog.
      4. While the second JFrame is active, launch a modal dialog (e.g. have a button that triggers a JOptionPane)
      5. When the modal dialog is dismissed the _FIRST_ window will be foregrounded, not the second (which had been previously-active).

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Window #2 is active
      ACTUAL -
      Window #1 is active

      ---------- BEGIN SOURCE ----------
      import java.awt.DefaultKeyboardFocusManager;
      import java.awt.KeyboardFocusManager;
      import javax.swing.JButton;
      import javax.swing.JFrame;
      import javax.swing.JOptionPane;
      import javax.swing.SwingUtilities;

      class Scratch {
          public static void main(String[] args) {
              JFrame testApplicationFrame = new JFrame("testApplicationFrame");
              testApplicationFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

              JButton openNewFrame = new JButton("Open new frame");
              openNewFrame.addActionListener((evt) -> {
                  JFrame secondFrame = new JFrame("secondFrame");
                  JButton showModalDialog = new JButton("Show modal dialog");
                  showModalDialog.addActionListener((e) -> {
                      JOptionPane.showConfirmDialog(showModalDialog, "When this dialog closes, 'secondFrame' should be activated...but it will incorrectly go to 'testApplicationFrame' on Windows.");
                  });
                  secondFrame.setContentPane(showModalDialog);
                  secondFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                  secondFrame.setSize(400, 200);
                  secondFrame.setVisible(true);
              });
              testApplicationFrame.setContentPane(openNewFrame);
              testApplicationFrame.setSize(400, 400);
              testApplicationFrame.setVisible(true);

              SwingUtilities.invokeLater(() -> {
                  // Swapping the KeyboardFocusManager causes WWindowPeer.ActiveWindowListener to no longer be
                  // attached to the current KFM, which causes the current (singleton) AppContext to have stale data
                  // stored at AppContext.get(KeyboardFocusManager.ACTIVE_WINDOWS_KEY). In the end, it means that
                  // when native function `AwtDialog::ModalActivateNextWindow` calls `WWindowPeer.getActiveWindowHandles()`,
                  // the stale value will be the only thing ever returned.
                  //
                  // As a consequence, when the modal closes, it will always take us back to 'testApplicationFrame'
                  // and not 'secondFrame' as we expect.
                  //
                  // If you remove the line of code that replaces the KFM, OR if it was invoked before 'testApplicationFrame'
                  // is created, then the post-modal activation works as expected.
                  KeyboardFocusManager.setCurrentKeyboardFocusManager(new DefaultKeyboardFocusManager());
              });
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      When replacing a KeyboardFocusManager, also manually copy over the PropertyChangeListeners from the to-be-replaced KFM.

      private static void replaceKeyboardFocusManager(KeyboardFocusManager newKeyboardFocusManager) {
          KeyboardFocusManager currentKeyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
          if (currentKeyboardFocusManager == newKeyboardFocusManager) {
              return;
          }

          // We need to replace the currentKeyboardFocusManager __BEFORE__ copying over listeners,
          // because replacing the KFM triggers a "managingFocus" event on both the old and new KFMs.
          // If we copy over first, these events will be incorrectly fired against both sets of KFMs.
          //
          // (The "managingFocus" event is, as far as I can tell, an event that is supposed to allow
          // any PropertyChangeListeners to move themselves over to the new KFM...but it seems to be
          // very sparsely implemented. And in a very relevant case, not implemented for WWindowPeer.ActiveWindowListener,
          // which is the ultimate root of the weird focus issue this function is made to fix.)
          KeyboardFocusManager.setCurrentKeyboardFocusManager(newKeyboardFocusManager);

          for (PropertyChangeListener listener : currentKeyboardFocusManager.getPropertyChangeListeners()) {
              // These 'instanceof' checks are very fragile, and likely need to be expanded to
              // handle cases depending on your particular application.
              // It is sufficient for my Java 8 environment.
              if (listener instanceof PropertyChangeListenerProxy) {
                  PropertyChangeListenerProxy proxyObj = (PropertyChangeListenerProxy) listener;
                  String propertyName = proxyObj.getPropertyName();
                  PropertyChangeListener backingListener = proxyObj.getListener();
                  newKeyboardFocusManager.addPropertyChangeListener(propertyName, backingListener);
              } else if (listener instanceof VetoableChangeListenerProxy) {
                  VetoableChangeListenerProxy proxyObj = (VetoableChangeListenerProxy) listener;
                  String propertyName = proxyObj.getPropertyName();
                  VetoableChangeListener backingListener = proxyObj.getListener();
                  newKeyboardFocusManager.addVetoableChangeListener(propertyName, backingListener);
              } else if (listener instanceof VetoableChangeListener) {
                  newKeyboardFocusManager.addVetoableChangeListener((VetoableChangeListener) listener);
              } else {
                  newKeyboardFocusManager.addPropertyChangeListener(listener);
              }
          }
      }

      FREQUENCY : always


        Attachments

          Activity

            People

            • Assignee:
              serb Sergey Bylokhov
              Reporter:
              webbuggrp Webbug Group
            • Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

              • Created:
                Updated: