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

Search

Online Books:
java.net on MarkMail:


Stephen Friedrich

Stephen Friedrich is a senior software engineer at Fortis IT-Services. He lives in the beautiful city of Hamburg, Germany, with his wife and five cats. Stephen worked his way from financial services' mainframe environment through distributed C++ in the telco business and has found his happy home in the Java community about six years ago. He developed special interests in both Java desktop technology and database connectivity while working on a number of commercial Java applications. When not busy coding he is found dancing and teaching Lindy Hop.

 

Stephen Friedrich's blog

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)
Syndicate content