Search code examples
javaswingmodal-dialogjdialog

After closing a modal JDialog, sometimes main window doesn't receive focus until another modal dialog is shown


I apologise ahead of time of lack of reproducable example, the app i have is very big, and the problem seems to be related to weird combination of controls and focus switches, which i haven't been able to reproduce with a shorter program.

Basically, i have an text editor application (in java8 on windows 7) with a JFrame and a JTabbedPane with each tab containing a JSplitPane of text area and another tab displaying some information. I have implemented a tab switcher system like in Eclipse, Ctrl-Tab shows a list of editors in a modal JDialog. This is implemented as a KeyListener which keeps track of various keys pressed. The code looks something like this:

private TabsDialog td = new TabsDialog(mainFrame, true);

void onKeyPressCtrlTab()
{
  td.setVisible(true); //display tab dialog
  //After closing dialog, select correct tab
  TabContent tc = td.getSelectedTab();
  switchToTab(tc);
}

void onKeyRelease()
{
    dispatchDialog();
}

void dispatchDialog()
{
  if (td.isVisible())
  {
  td.setVisible(false);
  }
}

The problem is, if i invoke onKeyPressCtrlTab and onKeyRelease in quick succession by pressing and releasing Ctrl-Tab, the focus system of the main application stops working, i can click on the main window and select text with the mouse, but the caret in text components isn't being shown at all. I can't type any text into any of the text components either. Also, all requestFocusInWindow calls are failing.

The problem is reproducable every time, but sometimes it takes 5 of Ctrl-Tabs, sometimes longer.

I've traced the problem to Component#requestFocusHelper, the following call always fail after the problem appears:

final boolean requestFocusHelper (boolean temporary,
                                     boolean focusedWindowChangeAllowed,
                                     CausedFocusEvent.Cause cause)
...
        boolean success = peer.requestFocus
            (this, temporary, focusedWindowChangeAllowed, time, cause); //fails

It seems that calling a new modal dialog (for example JOptionPane#showConfirmDialog) restores the focus system, but this is not a great solution.

I have spend two days on this issue, but haven't been able to find any good solution. It hasn't helped to dispose and re-create the TabsDialog on every call. I suspect there's something wrong with how the dialogs are cleaned up / enabled in Windows.

I think i'm doing all work on AWT thread so i doubt that's the issue. There are "some" things going on when switching tabs, i have a splitpane which is set / restored since each tab remembers the position, but still, nothing that should affect the focus system in my opinion. I have a uncatched exception in thread filter, so no exceptions are supressed as far as i can see.

Appreciate any thoughts on the problem, and a potential solution (even workaround that restores the focus would be great)

Here's a demonstration of how it's supposed to work:

https://gist.github.com/siggemannen/4affdff4b1892a15e481c626a190efab


Solution

  • A pure Swing solution would be to use Key Bindings, not a 3rd party tool to handle key events.

    As I understand your requirement, here is my simple implementation using Key Bindings:

    import javax.swing.*;
    import javax.swing.border.*;
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.event.*;
    import javax.swing.plaf.*;
    import java.util.*;
    
    public class TabbedPaneDialog extends JPanel
    {
        JTabbedPane tabbedPane;
    
        public TabbedPaneDialog()
        {
            setLayout( new BorderLayout() );
    
            tabbedPane = new JTabbedPane();
            add(tabbedPane);
    
            newTab( "File 1" );
            newTab( "File 2" );
            newTab( "File 3" );
            newTab( "File 4" );
            newTab( "File 5" );
    
            // remove Control+Tab as a Tab key
    
            Set newForwardKeys = new HashSet();
            newForwardKeys.add( KeyStroke.getKeyStroke("TAB") );
            tabbedPane.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, newForwardKeys);
    
            // Use Control+Tab to display dialog
    
            KeyStroke controlTab = KeyStroke.getKeyStroke("control TAB");
            InputMap im = tabbedPane.getInputMap(JTabbedPane.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
            im.put(controlTab, "showDialog");
            tabbedPane.getActionMap().put("showDialog", new DialogAction("Show Dialog", tabbedPane));
    
        }
    
        private void newTab(String text)
        {
            JTextArea textArea = new JTextArea(10, 30);
            textArea.setText(text);
            JScrollPane scrollPane = new JScrollPane( textArea );
            tabbedPane.add( scrollPane, text );
    
            // remove Control+Tab as a Tab key
    
            Set newForwardKeys = new HashSet();
            newForwardKeys.add( KeyStroke.getKeyStroke("TAB") );
            textArea.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, newForwardKeys);
    
            // Use Control+Tab to display dialog
    
            KeyStroke controlTab = KeyStroke.getKeyStroke("control TAB");
            InputMap im = textArea.getInputMap(JTabbedPane.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
            im.put(controlTab, "showDialog");
            textArea.getActionMap().put("showDialog", new DialogAction("Show Dialog", tabbedPane));
        }
    
        class DialogAction extends AbstractAction
        {
            private JTabbedPane tabbedPane;
    
            public DialogAction(String name, JTabbedPane tabbedPane)
            {
                putValue( Action.NAME, name );
                this.tabbedPane = tabbedPane;
            }
    
            public void actionPerformed(ActionEvent e)
            {
                SelectionDialog dialog = new SelectionDialog(tabbedPane);
            }
        }
    
        class SelectionDialog
        {
            private JTabbedPane tabbedPane;
    
            public SelectionDialog(JTabbedPane tabbedPane)
            {
                this.tabbedPane = tabbedPane;
    
                DefaultListModel<String> model = new DefaultListModel<>();
    
                for (int i = 0; i < tabbedPane.getTabCount(); i++)
                {
                    model.addElement( tabbedPane.getTitleAt(i) );
                }
    
                JList<String> list = new JList<>(model);
                list.setSelectedIndex( tabbedPane.getSelectedIndex() );
    
                Set newForwardKeys = new HashSet();
                newForwardKeys.add( KeyStroke.getKeyStroke("TAB") );
                list.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, newForwardKeys);
    
                KeyStroke controlTab = KeyStroke.getKeyStroke("control TAB");
                InputMap im = list.getInputMap(JList.WHEN_FOCUSED);
                im.put(controlTab, "nextTab");
                list.getActionMap().put("nextTab", new NextTabAction("Next Tab", list));
    
                KeyStroke releasedControlTab = KeyStroke.getKeyStroke("released CONTROL");
                im = list.getInputMap(JList.WHEN_FOCUSED);
                im.put(releasedControlTab, "closeDialog");
                list.getActionMap().put("closeDialog", new CloseDialogAction("Close Dialog", list, tabbedPane));
    
                Window window = SwingUtilities.windowForComponent(tabbedPane);
                JDialog dialog = new JDialog(window);
                dialog.add(new JScrollPane(list));
                dialog.pack();
                dialog.setLocationRelativeTo( tabbedPane );
                dialog.setVisible(true);
            }
    
            class NextTabAction extends AbstractAction
            {
                private JList list;
    
                public NextTabAction(String name, JList list)
                {
                    putValue( Action.NAME, name );
                    this.list = list;
                }
    
                public void actionPerformed(ActionEvent e)
                {
                    int selected = list.getSelectedIndex();
                    selected++;
    
                    if (selected == list.getModel().getSize())
                        selected = 0;
    
                    list.setSelectedIndex( selected );
                }
            }
    
            class CloseDialogAction extends AbstractAction
            {
                private JList list;
                private JTabbedPane tabbedPane;
    
                public CloseDialogAction(String name, JList list, JTabbedPane tabbedPane)
                {
                    putValue( Action.NAME, name );
                    this.list = list;
                    this.tabbedPane = tabbedPane;
                }
    
                public void actionPerformed(ActionEvent e)
                {
                    int selected = list.getSelectedIndex();
                    tabbedPane.setSelectedIndex( selected );
    
                    Window window = SwingUtilities.windowForComponent(list);
                    window.setVisible( false );
    
                    JScrollPane scrollPane = (JScrollPane)tabbedPane.getComponentAt( selected );
                    JTextArea textArea = (JTextArea)scrollPane.getViewport().getView();
                    textArea.requestFocusInWindow();
                }
            }
        }
    
        public static void main(String args[])
        {
            JFrame frame = new JFrame("TabbedPane Dialog");
            frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
            frame.add(new TabbedPaneDialog());
            frame.pack();
            frame.setVisible(true);
        }
    }