The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


Spice up Text Components with Keyboard Shortcuts

Posted by skelvin on February 14, 2006 at 7:22 AM PST

In a new Swing project I was missing the alternative set of cut/copy/paste shortcuts and also the ability to quickly delete all characters up to the start or end of the current word. I had done this before, but wasn't able to dig up the code again. Neither did Google find any code I could simply borrow. So, here's how I implemented those, both for your and for my future reference.

Usually to add new a shortcut you can modify a component's InputMap, by adding a KeyStroke that maps to any key, for example the string "cut". Then you modify the component's action map by adding a mapping from this key to an instance of an Action.

Unfortunately it is problematic to do so in a generic way for all instances and all types of text components.

After some discussion on the javadesktop forum it turned out that for text component you can use a shortcut: Simply add the action itself to the input map and it will be used directly.

Windows-style Cut/Copy/Paste shortcuts

  • Cut: Shift-Delete
  • Paste: Shift-Insert
  • Copy: Control-Insert

I discovered these quite late, but have been using it alot. Guess it's because with my personal system of writing with four and a half fingers I almost never use the right modifier keys and typing ctrl-x with using the left control key is awkward.

These are quite easy to implement, because we can just reuse the existing actions and only need new bindings.

Delete to start/end of word

  • Delete-to-Start-of-Word: Ctrl-Backspace
  • Delete-to-End-of-Word: Ctrl-Delete

Interestingly the first two editors I tried handle Ctrl-Delete slightly differently: UltraEdit deletes all characters up the the start of the next word while Idea deletes only up to the end of the current word.

I decided to implement the latter because it's consistent with the next two editors I checked: Both OpenOffice and MS Word only delete to end of word.

These were a little harder to implement, because custom actions are needed.

References

Keyboard Bindings in Swing [Sun Developer Network]
Swing: Understanding Input/Action Maps [javalobby]
Discussion in the javadesktop forum

Implementation

To add the shortcuts to the text components in your swing application, simply add a single line that is executed during startup:
        SwingUtils.addTextComponentActions();
Without further ado, here is the complete code, together with a simple ui that let's you test it:
package com.eekboom.xswing;

import javax.swing.*;
import javax.swing.text.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.*;

public class SwingUtils {
    // See http://forums.java.net/jive/thread.jspa?threadID=8503
    public static void addTextComponentActions() {
        // note: we can safely register all keystrokes even for password fields:
        // the cut/copy actions won't work (unless the client property
        // "JPasswordField.cutCopyAllowed" has been set on the component)
        // and the deleteTpPrevious/NextAction explicitly check for password fields.
        String[] keys = {"TextField", "FormattedTextField", "PasswordField",
                         "TextArea", "TextPane", "EditorPane"};
        registerActions(keys);
    }

    private static void registerActions(String[] propertyPrefixes) {
        DeleteToEndOfWordAction deleteToEndOfWordAction =
                new DeleteToEndOfWordAction();
        DeleteToStartOfWordAction deleteToStartOfWordAction =
                new DeleteToStartOfWordAction();
        DefaultEditorKit.CopyAction copyAction =
                new DefaultEditorKit.CopyAction();
        DefaultEditorKit.CutAction cutAction = new DefaultEditorKit.CutAction();
        DefaultEditorKit.PasteAction pasteAction =
                new DefaultEditorKit.PasteAction();

        for(int i = 0; i < propertyPrefixes.length; i++) {
            String propertyPrefix = propertyPrefixes[i];
            UIDefaults defaults = UIManager.getDefaults();
            Object o = defaults.get(propertyPrefix + ".focusInputMap");
            InputMap inputMap = (InputMap) o;

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT,
                                                InputEvent.SHIFT_MASK, false),
                         pasteAction);
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,
                                                InputEvent.SHIFT_MASK, false),
                         cutAction);
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT,
                                                InputEvent.CTRL_MASK, false),
                         copyAction);

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,
                                                InputEvent.CTRL_MASK, false),
                         deleteToStartOfWordAction);
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,
                                                InputEvent.CTRL_MASK, false),
                         deleteToEndOfWordAction);

        }
    }
}

class DeleteToStartOfWordAction extends TextAction {
    public static final String NAME = "delete-to-previous-word";

    public DeleteToStartOfWordAction() {
        super(NAME);
    }

    public void actionPerformed(ActionEvent e) {
        JTextComponent textComponent = getTextComponent(e);
        if(textComponent == null || !textComponent.isEditable()) {
            UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
            return;
        }

        try {
            Document document = textComponent.getDocument();
            Caret caret = textComponent.getCaret();
            int caretIndex = caret.getDot();
            int markIndex = caret.getMark();

            int selectionEndIndex = Math.max(caretIndex, markIndex);
            int selectionStartIndex = Math.min(caretIndex, markIndex);
            int startIndex =
                    getStartOfWordOffset(textComponent, selectionStartIndex);

            if(startIndex != selectionEndIndex) {
                document.remove(startIndex, selectionEndIndex - startIndex);
            }
            else if(caretIndex > 0) {
                UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
            }
        }
        catch(BadLocationException bl) {
            UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
        }
    }

    private int getStartOfWordOffset(JTextComponent target, int offset)
            throws BadLocationException
    {
        if(target instanceof JPasswordField) {
            return 0;
        }
        Element currentParagraph =
                Utilities.getParagraphElement(target, offset);
        int wordOffset = Utilities.getPreviousWord(target, offset);
        boolean isInPreviousParagraph =
                wordOffset < currentParagraph.getStartOffset();
        if(isInPreviousParagraph) {
            //noinspection UnnecessaryLocalVariable
            int endOfPreviousParagraph = Utilities
                    .getParagraphElement(target, wordOffset).getEndOffset() - 1;
            wordOffset = endOfPreviousParagraph;
        }
        return wordOffset;
    }
}

class DeleteToEndOfWordAction extends TextAction {
    public static final String NAME = "delete-to-next-word";

    public DeleteToEndOfWordAction() {
        super(NAME);
    }

    public void actionPerformed(ActionEvent e) {
        JTextComponent textComponent = getTextComponent(e);
        if(textComponent == null || !textComponent.isEditable()) {
            UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
            return;
        }

        try {
            Document document = textComponent.getDocument();
            Caret caret = textComponent.getCaret();
            int caretIndex = caret.getDot();
            int markIndex = caret.getMark();

            int selectionEndIndex = Math.max(caretIndex, markIndex);
            int selectionStartIndex = Math.min(caretIndex, markIndex);
            int endIndex = getEndOfWordOffset(textComponent, selectionEndIndex);

            if(endIndex != selectionEndIndex) {
                document.remove(selectionStartIndex,
                                endIndex - selectionStartIndex);
            }
            else if(caretIndex > 0) {
                UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
            }
        }
        catch(BadLocationException bl) {
            UIManager.getLookAndFeel().provideErrorFeedback(textComponent);
        }
    }

    private int getEndOfWordOffset(JTextComponent target, int offset)
            throws BadLocationException
    {
        Element currentPararaph = Utilities.getParagraphElement(target, offset);
        int currentParagraphEndOffset = currentPararaph.getEndOffset();
        if(target instanceof JPasswordField) {
            return currentParagraphEndOffset - 1;
        }
        int wordOffset = offset;
        try {
            int startOfNextWord = Utilities.getNextWord(target, offset);
            int endOfCurrentWord = Utilities.getWordEnd(target, offset);
            boolean isInWhiteSpace = startOfNextWord == endOfCurrentWord;
            if(isInWhiteSpace) {
                wordOffset = Utilities.getWordEnd(target, startOfNextWord);
            }
            else {
                wordOffset = endOfCurrentWord;
            }
            if(wordOffset >= currentParagraphEndOffset &&
               offset != currentParagraphEndOffset - 1)
            {
                wordOffset = currentParagraphEndOffset - 1;
            }
        }
        catch(BadLocationException badLocationException) {
            int end = target.getDocument().getLength();
            if(wordOffset != end) {
                if(offset != currentParagraphEndOffset - 1) {
                    wordOffset = currentParagraphEndOffset - 1;
                }
                else {
                    wordOffset = end;
                }
            }
            else {
                throw badLocationException;
            }
        }
        return wordOffset;
    }
}

class Main extends JFrame {
    int _rowIndex = 0;

    private Main() {
        getContentPane().setLayout(new GridBagLayout());

        addLabelAndComponent("Text Field", new JTextField(40), false);
        addLabelAndComponent("Formatted Text Field", new JFormattedTextField(),
                             false);
        addLabelAndComponent("Password Field", new JPasswordField(), false);
        addLabelAndComponent("Text Area", new JTextArea(40, 3), true);
        addLabelAndComponent("Text Pane", new JTextPane(), true);
        addLabelAndComponent("Editor Pane", new JEditorPane(), true);

        setBounds(100, 100, 600, 400);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    private void addLabelAndComponent(String text, JTextComponent component,
                                      boolean fillVertically)
    {
        addLabel(text);
        addTextComponent(component, fillVertically);
    }

    private void addLabel(String text) {
        GridBagConstraints c = new GridBagConstraints(0, _rowIndex, 1, 1, 0.0,
                                                      0.0,
                                                      GridBagConstraints.LINE_START,
                                                      GridBagConstraints.NONE,
                                                      new Insets(2, 2, 0, 0), 0,
                                                      0);
        getContentPane().add(new JLabel(text), c);
    }

    private void addTextComponent(JTextComponent component,
                                  boolean fillVertically)
    {
        int fill = fillVertically ? GridBagConstraints.BOTH :
                   GridBagConstraints.HORIZONTAL;
        double weighty = fillVertically ? 1.0 : 0.0;
        Insets insets = new Insets(2, 2, 0, 0);
        GridBagConstraints c = new GridBagConstraints(1, _rowIndex, 1, 1, 1.0,
                                                      weighty,
                                                      GridBagConstraints.NORTHWEST,
                                                      fill, insets, 0, 0);
        if(fillVertically) {
            getContentPane().add(new JScrollPane(component), c);
        }
        else {
            getContentPane().add(component, c);
        }
        ++_rowIndex;
    }

    public static void main(String[] args) {
        SwingUtils.addTextComponentActions();

        new Main();
    }
}
Related Topics >> Java Desktop      
Comments
Comments are listed in date ascending order (oldest first)