8

I'm embedding a JFileChooser in my program in my own frame with other custom components in the frame. Here's a design of my app as it may help visualize my issues:

How I'm using JFileChooser

If you can't tell, the lists directly under the JFrame titles are JFileChoosers. The way this is supposed to work is you assign shortcuts to destinations and then when you press those shortcut keys, the selected file moves to the destination.

My strategy for doing this is to assign the shortcut to a javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW scope of the InputMap of the entire frame.

But what's annoying is that something (I assume the JFileChooser) keeps on responding/absorbing key presses I don't want it to. For example, if I press Ctrl+C my shortcut action doesn't get run. I've tried this with the native Look and Feel (I'm using windows 7) and the default L&F and both situations have the same problem. I think it might be trying to do a copy action of the selected file in the JFileChooser because if I click on one of the buttons to force it to lose focus, all the sudden my Ctrl+C command does my action.

But, I'm not really sure how the JFileChooser is doing this. When I call getKeyListeners() on it, it returns an empty array. I've also tried clearing its input map for this key combination at all three scopes, and it still seems to be absorbing the keypress.

Can anyone give me some sample code that makes the JFileChooser ignore Ctrl+C? Also, it'd be helpful if someone could tell me how to debug problems like this in the future.


Here is some code of what I've tried so far. You can also use this to try to test this on your own, since this code compiles and runs, as-is:

package com.sandbox;

import javax.swing.*;
import java.awt.event.ActionEvent;

public class Sandbox {

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control C"), "println");
        panel.getActionMap().put("println", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("The JPanel action was performed!");
            }
        });

        panel.add(buildFileChooser());  //if you comment out this line, Ctrl+C does a println, otherwise my action is ignored.

        frame.setContentPane(panel);

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    private static JFileChooser buildFileChooser() {
        JFileChooser fileChooser = new JFileChooser();        
        fileChooser.getActionMap().clear(); //I've tried lots of ideas like this, but the JFileChooser still responds to Ctrl+C
        return fileChooser;
    }
}

UPDATE: I've gone as far as to recursively clear the inputMaps and remove the keyListeners of the JFileChooser and all of its child components and the JFileChooser still swallows my Ctrl+C command. Here's the code I've used to do this (I passed my JFileChooser into this):

private static void removeKeyboardReactors(JComponent root) {
    System.out.println("I'm going to clear the inputMap of: " + root);
    root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).clear();
    root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).clear();
    root.getInputMap(JComponent.WHEN_FOCUSED).clear();
    root.getActionMap().clear();

    if (root.getRootPane() != null) {
        removeKeyboardReactors(root.getRootPane());
    }

    for (KeyListener keyListener : root.getKeyListeners()) {
        root.removeKeyListener(keyListener);
    }

    for (Component component : root.getComponents()) {
        if (component instanceof JComponent) {
            removeKeyboardReactors((JComponent) component);
        } else if (component instanceof Container) {
            Container container = (Container) component;
            for (Component containerComponent : container.getComponents()) {
                if (containerComponent instanceof JComponent) {
                    removeKeyboardReactors((JComponent) containerComponent);
                } else {
                    System.out.println("This Container Component was not a JComponent: " + containerComponent);
                }
            }
        } else {
            System.out.println("This was not a JComponent: " + component);
        }
    }
}
Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • Not sure why you asked me to help here. You have an answer. Ctrl C doesn't work all the time because components like JTextField and JList map these Key Bindings. You have removed this binding from all the components on the file chooser so your code should work now. Of course the problem with this solution is that is will remove the Ctrl+C functionality from all components in your application, not just the components on the file chooser. I don't know of any other solution. – camickr Apr 30 '13 at 06:00
  • @camickr To be specific, I linked you to a comment I made on the answer I got. "I found a problem with this. If you toggle it to the detail view then click on a file it still swallows "Control C". But if you click on the textfield it doesn't which is good because it used to before. Any idea what's causing that?" – Daniel Kaplan Apr 30 '13 at 06:56

2 Answers2

4

the details view will still have a populated inputmap

I suspect the difference between the details view and the list view is that one uses a JTable the other a JList. So I would guess you only need to remove the bindings from the JTable of the details view.

This can be done without creating the details panel:

InputMap im = (InputMap)UIManager.get("Table.ancestorInputMap");
KeyStroke ctrlC = KeyStroke.getKeyStroke("control C");
//im.put(ctrlC, "none");
im.remove(ctrlC);

Again, it should be noted that this solution (along with the solution you currently have) will remove the default Ctrl+C functionality for all components, not just the ones instantiated for the JFileChooser.

Edit:

Shouldn't it only be removing it from the ones I remove it from?

Your code uses the getParent() method to get the InputMap that contains the binding. This InputMap is shared by all instances of a component. A component will only have unique bindings when you use:

component.getInputMap(...).put(...);

That is, the binding is added to the components InputMap, not its parents InputMap.

How did you know you could do this and this is the right thing to do

See UIManager Defaults. This list the defaults for the given LAF. I don't know if this is the right thing to do. As far as I know the effect is the same as the code you use now. This is just another way or removing a binding from an InputMap without needing an actual component to access the parents InputMap.

Second edit:

Some simple code to show the InputMaps are the same:

public static void main(String[] args)
{
    JButton first = new JButton("button");
    System.out.println(first.getInputMap().getParent());

    InputMap im = (InputMap) UIManager.get("Button.focusInputMap");
    System.out.println(im);
}
camickr
  • 321,443
  • 19
  • 166
  • 288
  • I don't mind that it's removing it from all, but I don't understand why it's removing it from all. Shouldn't it only be removing it from the ones I remove it from? – Daniel Kaplan Apr 30 '13 at 16:46
  • Question #2 (the more important one): How did you know you could do this and this is the right thing to do: `InputMap im = (InputMap)UIManager.get("Table.ancestorInputMap");` – Daniel Kaplan Apr 30 '13 at 16:47
  • *My* code is removing from the parent input maps. But *yours* isn't. You're just removing from "Table.ancestorInputMap" and you seem to be saying that yours will remove Ctrl+C from everything, too. Or maybe I just misunderstand the context you're implying. – Daniel Kaplan Apr 30 '13 at 17:10
  • 2
    @tieTYT, The "Table.ancestorInputMap" is the parent InputMap for all JTable components. So if you remove the binding from the UIManager, its like the binding never existed. See my second edit. Note, I changed the code to actually use im.remove(...) instead of im.put(...). This should more closely match your existing code. – camickr Apr 30 '13 at 17:59
2

Apparently InputMaps can have parents. So your purging of all built-in key "reactors" is not entirely complete. As you've probably guessed, Swing registers certain default keyboard bindings for itself on certain components. On Windows this often includes Ctrl+C, since that is the OS-standard hotkey for copying data to the clipboard.

This modified removeKeyboardReactors gets the System.out.println appearing for me:

private static void removeKeyboardReactors(JComponent root) {
    System.out.println("I'm going to clear the inputMap of: " + root);
    clearInputMap(root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT));
    clearInputMap(root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW));
    clearInputMap(root.getInputMap(JComponent.WHEN_FOCUSED));

    for (KeyListener keyListener : root.getKeyListeners()) {
        root.removeKeyListener(keyListener);
    }

    for (Component component : root.getComponents()) {
        if (component instanceof JComponent) {
            removeKeyboardReactors((JComponent) component);
        } else if (component instanceof Container) {
            Container container = (Container) component;
            for (Component containerComponent : container.getComponents()) {
                if (containerComponent instanceof JComponent) {
                    removeKeyboardReactors((JComponent) containerComponent);
                } else {
                    System.out.println("This Container Component was not a JComponent: " + containerComponent);
                }
            }
        } else {
            System.out.println("This was not a JComponent: " + component);
        }
    }
}

private static void clearInputMap(InputMap inputMap) {
    inputMap.clear();
    while ((inputMap = inputMap.getParent()) != null) {
        inputMap.clear();
    }
}

I had to remove this code from removeKeyboardReactors because it was causing a stack overflow:

if (root.getRootPane() != null) {
    removeKeyboardReactors(root.getRootPane());
}

The entire modified Sandbox class follows below. Hopefully this is enough to get you on your way. If you want the key-binding removal to be more key-specific, have a look at InputMap#remove(KeyStroke).

public class Sandbox
{

    public static void main(String[] args)
    {
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control C"), "println");
        panel.getActionMap().put("println", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                System.out.println("The JPanel action was performed!");
            }
        });

        JFileChooser fileChooser = buildFileChooser();
        panel.add(fileChooser); //if you comment out this line, Ctrl+C does a println, otherwise my action is ignored.

        frame.setContentPane(panel);

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.pack();

        removeKeyboardReactors(fileChooser);

        frame.setVisible(true);
    }

    private static JFileChooser buildFileChooser()
    {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.getActionMap().clear(); //I've tried lots of ideas like this, but the JFileChooser still responds to Ctrl+C
        return fileChooser;
    }

    private static void clearInputMap(InputMap inputMap)
    {
        inputMap.clear();
        while ((inputMap = inputMap.getParent()) != null)
        {
            inputMap.clear();
        }
    }

    private static void removeKeyboardReactors(JComponent root) {
        System.out.println("I'm going to clear the inputMap of: " + root);
        clearInputMap(root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT));
        clearInputMap(root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW));
        clearInputMap(root.getInputMap(JComponent.WHEN_FOCUSED));

        for (KeyListener keyListener : root.getKeyListeners()) {
            root.removeKeyListener(keyListener);
        }

        for (Component component : root.getComponents()) {
            if (component instanceof JComponent) {
                removeKeyboardReactors((JComponent) component);
            } else if (component instanceof Container) {
                Container container = (Container) component;
                for (Component containerComponent : container.getComponents()) {
                    if (containerComponent instanceof JComponent) {
                        removeKeyboardReactors((JComponent) containerComponent);
                    } else {
                        System.out.println("This Container Component was not a JComponent: " + containerComponent);
                    }
                }
            } else {
                System.out.println("This was not a JComponent: " + component);
            }
        }
    }
}
Alex
  • 11,451
  • 6
  • 37
  • 52
Mike Clark
  • 10,027
  • 3
  • 40
  • 54
  • I shall try this out soon, thanks for the answer. Do you have any advice on how to debug these kind of problems in the future? EG: is there some way to turn on "swing debugging" so that I could see a log message that says, "Ctrl+C was handled by X action in inputmap Y" – Daniel Kaplan Apr 29 '13 at 21:22
  • I found a problem with this. If you toggle it to the detail view then click on a file it still swallows "Control C". But if you click on the textfield it doesn't which is good because it used to before. Any idea what's causing that? – Daniel Kaplan Apr 30 '13 at 04:52
  • Apparently the JFileChooser has a FilePane. This object has a JPanel (or something) to display the current view and getComponents will only return the currently displayed on. That means if you clear the inputMap and THEN switch to the details view, the details view will still have a populated inputmap. I can solve this by switching to both views and clearing the input map in each, then going back to where you were, but I think that's hacky. – Daniel Kaplan Apr 30 '13 at 05:58
  • There's no simple debugging mechanism that I know of. The details view thing is a bit of a hack. I'll think about it and get back to you. – Mike Clark Apr 30 '13 at 09:29
  • You can use the UIManager to remove the binding, assuming you know which component is causing the problem. See my suggestion. – camickr Apr 30 '13 at 16:23