<?xml version="1.0" encoding="utf-8"?>
<feed version="0.3" xmlns="http://purl.org/atom/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en">
<title>Stephen Friedrich&apos;s Blog</title>
<link rel="alternate" type="text/html" href="http://weblogs.java.net/blog/skelvin/" />
<modified>2006-02-14T15:22:44Z</modified>
<tagline></tagline>
<id>tag:weblogs.java.net,2008:/blog/skelvin/177</id>
<generator url="http://www.movabletype.org/" version="3.01D">Movable Type</generator>
<copyright>Copyright (c) 2006, skelvin</copyright>
<entry>
<title>Spice up Text Components with Keyboard Shortcuts</title>
<link rel="alternate" type="text/html" href="http://weblogs.java.net/blog/skelvin/archive/2006/02/spice_up_text_c_1.html" />
<modified>2006-02-14T15:22:44Z</modified>
<issued>2006-02-14T15:22:34Z</issued>
<id>tag:weblogs.java.net,2006:/blog/skelvin/177.4109</id>
<created>2006-02-14T15:22:34Z</created>
<summary type="text/plain">Have you ever been missing shortcuts like Shift-Insert for Paste and Ctrl-Backspace for Delete-to-Start-of-Word? Here&apos;s how to add them.</summary>
<author>
<name>skelvin</name>

<email>stephen.friedrich@fortis-it.de</email>
</author>
<dc:subject>Community: JavaDesktop</dc:subject>
<content type="text/html" mode="escaped" xml:lang="en" xml:base="http://weblogs.java.net/blog/skelvin/">
<![CDATA[<p>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.</p>

<p>Usually to add new a shortcut you can modify a component's <code>InputMap</code>, by adding a <code>KeyStroke</code> 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.</p>

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

<p>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.</p>

<p><strong>Windows-style Cut/Copy/Paste shortcuts</strong></p>
<ul>
<li>Cut: Shift-Delete</li>
<li>Paste: Shift-Insert</li>
<li>Copy: Control-Insert</li>
</ul>
<p>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.</p>
<p>These are quite easy to implement, because we can just reuse the existing actions and only need new bindings.</p>

<p><strong>Delete to start/end of word</strong></p>
<ul>
<li>Delete-to-Start-of-Word: Ctrl-Backspace</li>
<li>Delete-to-End-of-Word: Ctrl-Delete</li>
</ul>

<p>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.</p>
<p>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.<p>

<p>These were a little harder to implement, because custom actions are needed.</p>

<p><strong>References</strong></p>

<a href="http://java.sun.com/products/jfc/tsc/special_report/kestrel/keybindings.html">Keyboard Bindings in Swing [Sun Developer Network]</a><br/>
<a href="http://www.javalobby.org/java/forums/t19402.html">Swing: Understanding Input/Action Maps [javalobby]</a><br/>
<a href="http://forums.java.net/jive/thread.jspa?threadID=8503">Discussion in the javadesktop forum</a><br/>

<p><strong>Implementation</strong></p>

To add the shortcuts to the text components in your swing application, simply add a single line that is executed during startup:
<pre style="line-height: 100%;font-family:monospace;background-color:#ffffff"><span style="color:#000000;background-color:#ffffff;">        SwingUtils.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">addTextComponentActions(</span><span style="color:#000000;background-color:#ffffff;">);</span></pre>

Without further ado, here is the complete code, together with a simple ui that let's you test it:

<pre style="line-height: 100%;font-family:monospace;background-color:#ffffff"><span style="color:#000080;background-color:#ffffff;font-weight:bold;">package</span><span style="color:#000000;background-color:#ffffff;"> com.eekboom.xswing;

</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> javax.swing.*;
</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> javax.swing.text.*;
</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> java.awt.event.InputEvent;
</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> java.awt.event.KeyEvent;
</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> java.awt.event.ActionEvent;
</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">import</span><span style="color:#000000;background-color:#ffffff;"> java.awt.*;

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

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">static</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> registerActions(String[] propertyPrefixes) {
        DeleteToEndOfWordAction deleteToEndOfWordAction =
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> DeleteToEndOfWordAction();
        DeleteToStartOfWordAction deleteToStartOfWordAction =
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> DeleteToStartOfWordAction();
        DefaultEditorKit.CopyAction copyAction =
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> DefaultEditorKit.CopyAction();
        DefaultEditorKit.CutAction cutAction = </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> DefaultEditorKit.CutAction();
        DefaultEditorKit.PasteAction pasteAction =
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> DefaultEditorKit.PasteAction();

        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">for</span><span style="color:#000000;background-color:#ffffff;">(</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> i = </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">; i &lt; propertyPrefixes.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;">length;</span><span style="color:#000000;background-color:#ffffff;"> i++) {
            String propertyPrefix = propertyPrefixes[i];
            UIDefaults defaults = UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getDefaults(</span><span style="color:#000000;background-color:#ffffff;">);
            Object o = defaults.get(propertyPrefix + </span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">".focusInputMap"</span><span style="color:#000000;background-color:#ffffff;">);
            InputMap inputMap = (InputMap) o;

            inputMap.put(KeyStroke.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getKeyStroke(</span><span style="color:#000000;background-color:#ffffff;">KeyEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">VK_INSERT,
</span><span style="color:#000000;background-color:#ffffff;">                                                InputEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">SHIFT_MASK,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">),
                         pasteAction);
            inputMap.put(KeyStroke.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getKeyStroke(</span><span style="color:#000000;background-color:#ffffff;">KeyEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">VK_DELETE,
</span><span style="color:#000000;background-color:#ffffff;">                                                InputEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">SHIFT_MASK,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">),
                         cutAction);
            inputMap.put(KeyStroke.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getKeyStroke(</span><span style="color:#000000;background-color:#ffffff;">KeyEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">VK_INSERT,
</span><span style="color:#000000;background-color:#ffffff;">                                                InputEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">CTRL_MASK,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">),
                         copyAction);

            inputMap.put(KeyStroke.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getKeyStroke(</span><span style="color:#000000;background-color:#ffffff;">KeyEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">VK_BACK_SPACE,
</span><span style="color:#000000;background-color:#ffffff;">                                                InputEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">CTRL_MASK,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">),
                         deleteToStartOfWordAction);
            inputMap.put(KeyStroke.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getKeyStroke(</span><span style="color:#000000;background-color:#ffffff;">KeyEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">VK_DELETE,
</span><span style="color:#000000;background-color:#ffffff;">                                                InputEvent.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">CTRL_MASK,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">),
                         deleteToEndOfWordAction);

        }
    }
}

</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">class</span><span style="color:#000000;background-color:#ffffff;"> DeleteToStartOfWordAction </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">extends</span><span style="color:#000000;background-color:#ffffff;"> TextAction {
    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">static</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">final</span><span style="color:#000000;background-color:#ffffff;"> String </span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NAME </span><span style="color:#000000;background-color:#ffffff;">= </span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"delete-to-previous-word"</span><span style="color:#000000;background-color:#ffffff;">;

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> DeleteToStartOfWordAction() {
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">super</span><span style="color:#000000;background-color:#ffffff;">(</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NAME)</span><span style="color:#000000;background-color:#ffffff;">;
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> actionPerformed(ActionEvent e) {
        JTextComponent textComponent = getTextComponent(e);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(textComponent == </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">null</span><span style="color:#000000;background-color:#ffffff;"> || !textComponent.isEditable()) {
            UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;">;
        }

        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">try</span><span style="color:#000000;background-color:#ffffff;"> {
            Document document = textComponent.getDocument();
            Caret caret = textComponent.getCaret();
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> caretIndex = caret.getDot();
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> markIndex = caret.getMark();

            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> selectionEndIndex = Math.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">max(</span><span style="color:#000000;background-color:#ffffff;">caretIndex, markIndex);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> selectionStartIndex = Math.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">min(</span><span style="color:#000000;background-color:#ffffff;">caretIndex, markIndex);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> startIndex =
                    getStartOfWordOffset(textComponent, selectionStartIndex);

            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(startIndex != selectionEndIndex) {
                document.remove(startIndex, selectionEndIndex - startIndex);
            }
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(caretIndex &gt; </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">) {
                UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
            }
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">catch</span><span style="color:#000000;background-color:#ffffff;">(BadLocationException bl) {
            UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
        }
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> getStartOfWordOffset(JTextComponent target, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> offset)
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">throws</span><span style="color:#000000;background-color:#ffffff;"> BadLocationException
    {
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(target </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">instanceof</span><span style="color:#000000;background-color:#ffffff;"> JPasswordField) {
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">;
        }
        Element currentParagraph =
                Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getParagraphElement(</span><span style="color:#000000;background-color:#ffffff;">target, offset);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> wordOffset = Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getPreviousWord(</span><span style="color:#000000;background-color:#ffffff;">target, offset);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">boolean</span><span style="color:#000000;background-color:#ffffff;"> isInPreviousParagraph =
                wordOffset &lt; currentParagraph.getStartOffset();
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(isInPreviousParagraph) {
            </span><span style="color:#808080;background-color:#ffffff;font-style:italic;">//noinspection UnnecessaryLocalVariable
</span><span style="color:#000000;background-color:#ffffff;">            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> endOfPreviousParagraph = Utilities
                    .</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getParagraphElement(</span><span style="color:#000000;background-color:#ffffff;">target, wordOffset).getEndOffset() - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">;
            wordOffset = endOfPreviousParagraph;
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;"> wordOffset;
    }
}

</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">class</span><span style="color:#000000;background-color:#ffffff;"> DeleteToEndOfWordAction </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">extends</span><span style="color:#000000;background-color:#ffffff;"> TextAction {
    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">static</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">final</span><span style="color:#000000;background-color:#ffffff;"> String </span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NAME </span><span style="color:#000000;background-color:#ffffff;">= </span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"delete-to-next-word"</span><span style="color:#000000;background-color:#ffffff;">;

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> DeleteToEndOfWordAction() {
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">super</span><span style="color:#000000;background-color:#ffffff;">(</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NAME)</span><span style="color:#000000;background-color:#ffffff;">;
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> actionPerformed(ActionEvent e) {
        JTextComponent textComponent = getTextComponent(e);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(textComponent == </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">null</span><span style="color:#000000;background-color:#ffffff;"> || !textComponent.isEditable()) {
            UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;">;
        }

        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">try</span><span style="color:#000000;background-color:#ffffff;"> {
            Document document = textComponent.getDocument();
            Caret caret = textComponent.getCaret();
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> caretIndex = caret.getDot();
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> markIndex = caret.getMark();

            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> selectionEndIndex = Math.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">max(</span><span style="color:#000000;background-color:#ffffff;">caretIndex, markIndex);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> selectionStartIndex = Math.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">min(</span><span style="color:#000000;background-color:#ffffff;">caretIndex, markIndex);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> endIndex = getEndOfWordOffset(textComponent, selectionEndIndex);

            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(endIndex != selectionEndIndex) {
                document.remove(selectionStartIndex,
                                endIndex - selectionStartIndex);
            }
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(caretIndex &gt; </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">) {
                UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
            }
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">catch</span><span style="color:#000000;background-color:#ffffff;">(BadLocationException bl) {
            UIManager.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getLookAndFeel(</span><span style="color:#000000;background-color:#ffffff;">).provideErrorFeedback(textComponent);
        }
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> getEndOfWordOffset(JTextComponent target, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> offset)
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">throws</span><span style="color:#000000;background-color:#ffffff;"> BadLocationException
    {
        Element currentPararaph = Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getParagraphElement(</span><span style="color:#000000;background-color:#ffffff;">target, offset);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> currentParagraphEndOffset = currentPararaph.getEndOffset();
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(target </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">instanceof</span><span style="color:#000000;background-color:#ffffff;"> JPasswordField) {
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;"> currentParagraphEndOffset - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">;
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> wordOffset = offset;
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">try</span><span style="color:#000000;background-color:#ffffff;"> {
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> startOfNextWord = Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getNextWord(</span><span style="color:#000000;background-color:#ffffff;">target, offset);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> endOfCurrentWord = Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getWordEnd(</span><span style="color:#000000;background-color:#ffffff;">target, offset);
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">boolean</span><span style="color:#000000;background-color:#ffffff;"> isInWhiteSpace = startOfNextWord == endOfCurrentWord;
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(isInWhiteSpace) {
                wordOffset = Utilities.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">getWordEnd(</span><span style="color:#000000;background-color:#ffffff;">target, startOfNextWord);
            }
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> {
                wordOffset = endOfCurrentWord;
            }
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(wordOffset &gt;= currentParagraphEndOffset &&
               offset != currentParagraphEndOffset - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">)
            {
                wordOffset = currentParagraphEndOffset - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">;
            }
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">catch</span><span style="color:#000000;background-color:#ffffff;">(BadLocationException badLocationException) {
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> end = target.getDocument().getLength();
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(wordOffset != end) {
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(offset != currentParagraphEndOffset - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">) {
                    wordOffset = currentParagraphEndOffset - </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">;
                }
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> {
                    wordOffset = end;
                }
            }
            </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> {
                </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">throw</span><span style="color:#000000;background-color:#ffffff;"> badLocationException;
            }
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">return</span><span style="color:#000000;background-color:#ffffff;"> wordOffset;
    }
}

</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">class</span><span style="color:#000000;background-color:#ffffff;"> Main </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">extends</span><span style="color:#000000;background-color:#ffffff;"> JFrame {
    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;">_rowIndex </span><span style="color:#000000;background-color:#ffffff;">= </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">;

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> Main() {
        getContentPane().setLayout(</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> GridBagLayout());

        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Text Field"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JTextField(</span><span style="color:#0000ff;background-color:#ffffff;">40</span><span style="color:#000000;background-color:#ffffff;">), </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">);
        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Formatted Text Field"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JFormattedTextField(),
                             </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">);
        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Password Field"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JPasswordField(), </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">false</span><span style="color:#000000;background-color:#ffffff;">);
        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Text Area"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JTextArea(</span><span style="color:#0000ff;background-color:#ffffff;">40</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">3</span><span style="color:#000000;background-color:#ffffff;">), </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">true</span><span style="color:#000000;background-color:#ffffff;">);
        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Text Pane"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JTextPane(), </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">true</span><span style="color:#000000;background-color:#ffffff;">);
        addLabelAndComponent(</span><span style="color:#008000;background-color:#ffffff;font-weight:bold;">"Editor Pane"</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JEditorPane(), </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">true</span><span style="color:#000000;background-color:#ffffff;">);

        setBounds(</span><span style="color:#0000ff;background-color:#ffffff;">100</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">100</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">600</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">400</span><span style="color:#000000;background-color:#ffffff;">);
        setDefaultCloseOperation(JFrame.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">EXIT_ON_CLOSE)</span><span style="color:#000000;background-color:#ffffff;">;
        setVisible(</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">true</span><span style="color:#000000;background-color:#ffffff;">);
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> addLabelAndComponent(String text, JTextComponent component,
                                      </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">boolean</span><span style="color:#000000;background-color:#ffffff;"> fillVertically)
    {
        addLabel(text);
        addTextComponent(component, fillVertically);
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> addLabel(String text) {
        GridBagConstraints c = </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> GridBagConstraints(</span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;">_rowIndex,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0.0</span><span style="color:#000000;background-color:#ffffff;">,
                                                      </span><span style="color:#0000ff;background-color:#ffffff;">0.0</span><span style="color:#000000;background-color:#ffffff;">,
                                                      GridBagConstraints.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">LINE_START,
</span><span style="color:#000000;background-color:#ffffff;">                                                      GridBagConstraints.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NONE,
</span><span style="color:#000000;background-color:#ffffff;">                                                      </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> Insets(</span><span style="color:#0000ff;background-color:#ffffff;">2</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">2</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">), </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">,
                                                      </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">);
        getContentPane().add(</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JLabel(text), c);
    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">private</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> addTextComponent(JTextComponent component,
                                  </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">boolean</span><span style="color:#000000;background-color:#ffffff;"> fillVertically)
    {
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">int</span><span style="color:#000000;background-color:#ffffff;"> fill = fillVertically ? GridBagConstraints.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">BOTH </span><span style="color:#000000;background-color:#ffffff;">:
                   GridBagConstraints.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">HORIZONTAL;
</span><span style="color:#000000;background-color:#ffffff;">        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">double</span><span style="color:#000000;background-color:#ffffff;"> weighty = fillVertically ? </span><span style="color:#0000ff;background-color:#ffffff;">1.0</span><span style="color:#000000;background-color:#ffffff;"> : </span><span style="color:#0000ff;background-color:#ffffff;">0.0</span><span style="color:#000000;background-color:#ffffff;">;
        Insets insets = </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> Insets(</span><span style="color:#0000ff;background-color:#ffffff;">2</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">2</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">);
        GridBagConstraints c = </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> GridBagConstraints(</span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;">_rowIndex,</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">1</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">1.0</span><span style="color:#000000;background-color:#ffffff;">,
                                                      weighty,
                                                      GridBagConstraints.</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;font-style:italic;">NORTHWEST,
</span><span style="color:#000000;background-color:#ffffff;">                                                      fill, insets, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">, </span><span style="color:#0000ff;background-color:#ffffff;">0</span><span style="color:#000000;background-color:#ffffff;">);
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">if</span><span style="color:#000000;background-color:#ffffff;">(fillVertically) {
            getContentPane().add(</span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> JScrollPane(component), c);
        }
        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">else</span><span style="color:#000000;background-color:#ffffff;"> {
            getContentPane().add(component, c);
        }
        ++</span><span style="color:#660e7a;background-color:#ffffff;font-weight:bold;">_rowIndex;
</span><span style="color:#000000;background-color:#ffffff;">    }

    </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">public</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">static</span><span style="color:#000000;background-color:#ffffff;"> </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">void</span><span style="color:#000000;background-color:#ffffff;"> main(String[] args) {
        SwingUtils.</span><span style="color:#000000;background-color:#ffffff;font-style:italic;">addTextComponentActions(</span><span style="color:#000000;background-color:#ffffff;">);

        </span><span style="color:#000080;background-color:#ffffff;font-weight:bold;">new</span><span style="color:#000000;background-color:#ffffff;"> Main();
    }
}
</span><span style="color:#000000;background-color:#ffcccc;">]]>

</content>
</entry>
<entry>
<title>Natural String Order</title>
<link rel="alternate" type="text/html" href="http://weblogs.java.net/blog/skelvin/archive/2006/01/natural_string.html" />
<modified>2006-01-13T14:53:25Z</modified>
<issued>2006-01-13T14:53:16Z</issued>
<id>tag:weblogs.java.net,2006:/blog/skelvin/177.3933</id>
<created>2006-01-13T14:53:16Z</created>
<summary type="text/plain"><![CDATA[Make string comparisons a little more clever by correctly sorting contained numbers. Hey, Windows XP finally sorts files intelligently, e.g. picture 9.jpg &lt; picture 10.jpg &lt; picture 11.jpg.
So your Java software should be able to avoid stupidities like picture 1.jpg &lt; picture 10.jpg &lt; picture 2.jpg]]></summary>
<author>
<name>skelvin</name>

<email>stephen.friedrich@fortis-it.de</email>
</author>
<dc:subject>J2SE</dc:subject>
<content type="text/html" mode="escaped" xml:lang="en" xml:base="http://weblogs.java.net/blog/skelvin/">
<![CDATA[<p>Treats contained numbers sensible when comparing strings</p>

<table border=0 cellspacing="10">
    <tr>
        <td>
            <p><strong>Natural Order</strong><br/>(e.g. implemented in Windows XP's file explorer)</p>
            <pre style="background:#F0F0C0">

rfc 1.txt
rfc 822.txt
rfc 2086.txt
            </pre>
        </td>
        <td>
            <p><strong>Java's default Order</strong><br/>Somewhat stupid...</p>
            <pre style="background:#F0F0C0">

rfc 1.txt
rfc 2086.txt
rfc 822.txt
            </pre>
        </td>
    </tr>
    <tr><td colspan="2"><p>(Kudos to Martin Pool for the example.)</p></td> </tr>
</table>

<p>When I needed a natural sort feature in my Java app I started to google and to look in a few other places
(like Apache Commons).
There should be plenty of code out there that does this, right? Nope.
I found a page with a description of the problem and links to implementations in a couple of programming languages:
<a href="http://sourcefrog.net/projects/natsort/">Natural Order String Comparison</a></p>
<p>There's a single Java implementation, but that one cannot handle texts that contain non-ascii characters.
So it would fail in my German application, where texts contain funny characters such as these:<br/>
<span style="background:#F0F0C0">Im <strong>Ü</strong>brigen macht's mehr Spa<strong>ß</strong>, es selbst zu schreiben.</span>
</p>

<p><strong>The code</strong></p>
<p>Consists of a couple of static methods in class Strings: <a href="http://www.eekboom.com/java/compareNatural/src/com/eekboom/utils/Strings.java">Strings.java</a><br/>
Here is the <a href="http://www.eekboom.com/java/compareNatural/javadoc/com/eekboom/utils/Strings.html">Javadoc</a>.<br/>
Some junit tests: <a href="http://www.eekboom.com/java/compareNatural/src_test/com/eekboom/utils/TestStrings.java">TestStrings.java</a><br/></p>

<p>Basically there are some variants of methods <code>compareNatural...(String, String)</code> that directly compare
strings and convenience function, if you need a string <code>Comparator</code>: <code>getNaturalComparator...()</code>.
The different variants either use either the current default locale's comparison rules, allow to specify a <code>Collator</code>
to use or work on 7-bit ascii characters only.</p>
<p>Java 1.5: There is a little bit of generics used (e.g. <code>Comparator&lt;String&gt;</code>). It's easy to change
if you require the code to work on older Java versions.</p>

<p><strong>Basics and Intricacies</strong></p>
<p>Basically natural comparison works by breaking each string into parts where each part is either a number or text-only.
Number parts are compared according to their corresponding numeric value while text parts are compared lexicographically.
However simple this seems, there are quite some subtleties to handle.<br/>
A nice, detailed explanation is also given in this spec from the OpenOffice project:
<a href="http://specs.openoffice.org/calc/ease-of-use/natural_sort_algorithm.sxw">Natural Sort Option in Sort Dialog</a>
</p>

<p><strong>Tokenizing</strong></p>
<p>Exactly how to determine which parts of a text are numbers can be surprisingly difficult. The two complicating issues
are non-integer numbers and internationalization.</p>
<p>My implementation handles only integer numbers, so "a 1.3" is considered less than "a 1.11" because each is broken
into four parts ("a ", "1", "." and "3" resp. "11") and 3 is less than 11.
The need to handle fractional numbers should arise very rarely and trying to handle these opens a can of worms
(different decimal point representations in different locales, scientific formats, ...).</p>
<p>Digits are defined differently in different locales. Currently my implementation starts a new token whenever the
value of Character.isDigit(c) changes.</p>

<p><strong>Internationalization</strong></p>
<p>My minimum requirement was to compare the text parts of the strings using locale specific rules (which means you can
specify a <code>java.text.Collator</code>).
The code should work fine for any language that uses the number characters '0'...'9'.</p>
<p>Other number systems are not tested (they will probably not work, because I do not really convert the
number to a numeric value, but compare each digit separately).</p>

<p><strong>Supplementary characters</strong></p>
<p>Once upon a time it seemed to me that Java's native unicode support was the end of all character set problems.
No more, no more: The complete Unicode definition now contains character that cannot be represented by a single (16 bit)
char variable, see this article from sun
<a href="http://java.sun.com/developer/technicalArticles/Intl/Supplementary/">http://java.sun.com/developer/technicalArticles/Intl/Supplementary/</a></p>
<p>These supplementary characters are simply not handled correctly in my natural comparison implementation.</p>

<p><strong>Numbers with leading zeros</strong></p>
<p>How should the strings "p0002", "p2", "p02" be sorted?<br/>
Strictly comparing the numeric values of the number parts would leave the sort order undefined, as each of these strings
will be considered equal.</p>
<p>I have chosen to sort by increasing numbers of leading zeros: "p2", "p02", "p0002". That way <code>Strings.getNaturalComparator().compare(String, String)</code>
and <code>String.equals(String)</code> are still compatible: <code>compare()</code> will return 0 exactly if <code>equals()</code>
returns true.
</p>

<p><strong>Whitespace</strong></p>
<p>How do "p4" and "p  3" compare?</p>
<p>I have choosen not to ignore whitespace, so "p4" is less than "p  3". I'd like to be alerted to difference in
the naming scheme, so I think this is reasonable. Maybe later I'll add an option to control the handling of whitespace.</p>

<p><strong>Performance</strong></p>
<p>First of all I don't think performance really matters in the typical application of natural comparison. You probably
don't have hundreds of thousands of files in the same directory whose names differ only in a number, do you?</p>
<p>Still, the very simple approach of breaking the string into parts (effectively creating two list of strings) and then
comparing each string separately might be too costly.
So I took care to avoid creating new instances where possible and do all comparisons in place.
For many scenarios the natural comparison is about as fast as Java's String.compareTo() method. If there are many
strings that have a common prefix and contain a number that differs in trailing digits only the natural comparison
is about four times slower.</p>]]>

</content>
</entry>
<entry>
<title>Big Severe Logging with Ascii Art</title>
<link rel="alternate" type="text/html" href="http://weblogs.java.net/blog/skelvin/archive/2004/08/big_severe_logg.html" />
<modified>2008-01-02T17:42:16Z</modified>
<issued>2004-08-24T11:46:11Z</issued>
<id>tag:weblogs.java.net,2004:/blog/skelvin/177.1105</id>
<created>2004-08-24T11:46:11Z</created>
<summary type="text/plain">A little Java 2D can give your log messages much bigger visibility. Or find yourself your own excuse to have some fun with ascii art graphics using Java 2D...</summary>
<author>
<name>skelvin</name>

<email>stephen.friedrich@fortis-it.de</email>
</author>
<dc:subject>J2SE</dc:subject>
<content type="text/html" mode="escaped" xml:lang="en" xml:base="http://weblogs.java.net/blog/skelvin/">
<![CDATA[<p>A little Java 2D can give your log messages much bigger visibility. Or find yourself your own
excuse to have some fun with ascii art graphics using Java 2D...</p>

<pre>
 #########            ##  ##                             ##  
 #########            ##  ##                             ##  
 ##                       ##                             ##  
 ##           #####   ##  ##  ##    ##  ## ##   ####     ##  
 ##          #######  ##  ##  ##    ##  #####  ######    ##  
 ########   ##    ##  ##  ##  ##    ##  ###   ###  ##    ##  
 ########       ####  ##  ##  ##    ##  ##    ##    ##   ##  
 ##          #######  ##  ##  ##    ##  ##    ########   ##  
 ##         ####  ##  ##  ##  ##    ##  ##    ########   ##  
 ##         ##    ##  ##  ##  ##    ##  ##    ##         ##  
 ##         ##   ###  ##  ##  ##   ###  ##    ###   ##       
 ##         ########  ##  ##  ########  ##     ######    ##  
 ##          #### ##  ##  ##   #### ##  ##      ####     ##  
</pre>
<p>I consider myself a lucky guy, being allowed to work full-time with Java and get paid for it.
Only occasionally I do miss the chance to step back a little from business demands and just
write some cool, yet more or less useless code.</p>
<p>So I jumped to the occasion when a day's work was almost done and I had the mean idea of putting
something cool to use: I just had spent some thirty minutes tracing a bug when I noticed that
single message in the enormous log file that revealed useful information. I cursed myself 
for not noticing earlier that it was logged as "SEVERE" and caught myself thinking
"Couldn't the fellow who put that log message in there have done it in 40 pixels big bold face?"
Well, of course not, it's plain ascii text, ... but hey, wait - I can do that!</p>
<p>Those of you who have been around long enough probably remember the novelty services that accepted a
small photo and handed back to you an ascii print out that let you recognize the photo - if you
stepped back far enough for the black and white to get blurred into a gray distribution.
We really have come a long way with real-time photo-realistic graphics and powerful APIs like
Java 2D and 3D!</p>

<p>Well, right now I could use some very simple, black and white ascii art:
Create an image, draw the string to it, get the pixel raster and print out a space for a white
pixel and some other character for a black pixel. Ten minutes' job.</p>
<p>In fact, it turned out to be two hours fumbling with Java 2D and the logging system.
But that was due to my partial knowledge of those APIs on the one hand, and my inherent desire to 
go beyond the quick and dirty initial implementation and deal correctly with line breaks, tabs
and null text on the other hand. Well, at least I stopped short of trying to support right-to-left,
bottom-to-top text. I'm leaving that as an exercise for the reader ;-)</p>

<p><b>Text to Ascii Art</b></p>

<p>So let's have a short walk through the most important parts of the code.<br>
I'd like to have something like this:</p>
<code><pre><b style="color:#000080">public</b> static String getAsciiArt(Font font, String text) {</pre></code>
<p>Calling <code>getAsciiArt(<b style="color:#000080">new</b> Font(<b style="font-weight:normal;color:#008000">"SansSerif"</b>, Font.PLAIN, <b style="font-weight:normal;color:#0000ff">18</b>), <b style="font-weight:normal;color:#008000">"Failure!"</b>)</code> should give the string
you see above.</p>

<p>On we go: Before you can do anything sensible with text as graphics, you need a <code>FontRenderContext</code> to
specify exactly how a font is mapped to a pixel raster:</p>
<code><pre>            FontRenderContext fontRenderContext = <b style="color:#000080">new</b> FontRenderContext(<b style="color:#000080">null</b>, false, false);</pre></code>
<p>The output does not need to be transformed in any way (rotated, scaled, ...), so passing null
gets me an <code>IdentityTransform</code>. The two <code>false</code> literals specify no anti-aliasing and
non-fractional metrics (Does not make sense when you think of a pixel as a character, right?)
The usual way to get a <code>FontRenderContext</code> instance is from the <code>Graphics2D</code> object passed into 
any <code>paint()</code> method of components, but this isn't the usual paint application, anyway.</p>

<p>Now before the text can be drawn into an image I need to know the image size. That resulted
in some heavy application of the engineering pattern called trial and error:<br>
Most methods (e.g. <code>TextLayout.getBounds()</code>) were not working as expected, e.g. ignoring white
space at the text's start or end (which makes some sense, there's nothing to draw there).
Finally I found that creating a <code>GlyphVector</code> and using its logical bounds did what I wanted:</p>
<code><pre>                GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, text);
                Rectangle2D bounds      = glyphVector.getLogicalBounds();</pre></code>
                
<p>Next, simply create an image to paint to:</p>
<code><pre>                BufferedImage image = <b style="color:#000080">new</b> BufferedImage((<b style="color:#000080">int</b>) bounds.getWidth(), 
                                                        (<b style="color:#000080">int</b>) bounds.getHeight(),
                                                        BufferedImage.TYPE_BYTE_BINARY);</pre></code>

<p>Drawing into the image is quick, just get a graphics object from the image and use it
to draw to the image:</p>
<code><pre>                Graphics2D graphics = image.createGraphics();
                graphics.drawGlyphVector(glyphVector, <b style="font-weight:normal;color:#0000ff">0</b>, ascent);
</pre></code>
<p>I haven't explained the "ascent" yet, so what's that about? The <code>drawGlyphVector()</code> method
does not document at all what the x/y position passed into it is relative to. I expected
it to be the baseline, i.e. that line you write on in a lined text book with characters
like "y" extending further down. That assumption was correct, but unfortunately the <code>GlyphVector</code>
class has no easy way to retrieve the baseline.</p>
<p>Well, <code>TextLayout</code> seems to supply what I needed:</p>
<code><pre>                TextLayout textLayout = <b style="color:#000080">new</b> TextLayout(text, font, fontRenderContext);</pre></code>
<p>Grmpf! <code>textLayout.getBaseline()</code> does not return the actual position of the baseline, but rather
its <i>type</i> - a stupid name for this method. I only learned about different types of baselines
after some unsuccessful attempts to position the text and I'd rather not elaborate on that here.
Still, <code>TextLayout</code> has a nice method called <code>getAscent()</code>, which is just what I need - the delta
from the top of the bounds to the baseline:</p>
<code><pre>                <b style="color:#000080">float</b> ascent = textLayout.getAscent();</pre></code>
<p>What's left is to get the actual pixel raster and iterate over pixels appending characters
to the log:</p>
<code><pre>                    Raster raster = image.getRaster();
                    ...
                    raster.getPixel(x, y, ints);
                    <b style="color:#000080">int</b> pixelValue = ints[<b style="font-weight:normal;color:#0000ff">0</b>];
                    buffer.append(pixelValue > <b style="font-weight:normal;color:#0000ff">0</b> ? <b style="font-weight:normal;color:#008000">'#'</b> : <b style="font-weight:normal;color:#008000">' '</b>);
</pre></code>
<p>There actually is quite a bit more code, but you can have a look at that for yourself in the complete
implementation below.</p>

<p><b>Logging in Ascii Art</b></p>

<p>We are using our own wrapper around log4j, but for this blog I'll stick to the standard Java
logging mechanism (from <code>java.util.logging</code>).</p>
<p>Before any text is actually "published" (the log system's lingo for finally outputting it),
it can be formatted. Well, that's nice because our ascii art output can be easily implemented
as a <code>Formatter</code>. On second thought, I make that a <code>Formatter</code> that wraps another <code>Formatter</code>, so
that the usual formatting options (which may include timestamp, thread name etc.) aren't lost.</p>

<p>So let's supply a base formatter instance plus a font used for ascii art and a minimum log level
to begin logging in ascii art. Oh yeah, and let those arguments default to something sensible
if nulls are passed:</p>

<code><pre><b style="color:#000080">public</b> class AsciiArtFormatter extends Formatter {
    <b style="color:#000080">public</b> AsciiArtFormatter(Formatter formatter, Level level, Font font) {
        <b style="color:#000080">this</b>.formatter = (formatter == <b style="color:#000080">null</b> ? <b style="color:#000080">new</b> SimpleFormatter() : formatter);
        <b style="color:#000080">this</b>.level     = level == <b style="color:#000080">null</b> ? Level.SEVERE : level;
        <b style="color:#000080">this</b>.font      = font == <b style="color:#000080">null</b> ? <b style="color:#000080">new</b> Font(<b style="font-weight:normal;color:#008000">"SansSerif"</b>, Font.PLAIN, <b style="font-weight:normal;color:#0000ff">18</b>) : font;
    }
</pre></code>
The one method that has to be implemented is, of course, format():

<code><pre>    <b style="color:#000080">public</b> String format(LogRecord record) {
        String message = formatter.format(record);
        <b style="color:#000080">if</b>(record.getLevel().intValue() >= level.intValue()) {
            message = message + getAsciiArt(font, record.getMessage());
        }
        <b style="color:#000080">return</b> message;
    }
</pre></code>
<p>I have included the complete message (as obtained from the wrapper formatter's <code>format()</code> method) so that
the text can be found if you search the log file using your favorite text editor and then append
the ascii art string to it. The ascii art is indeed graphics - with quite low resolution - so that if
only that were output, you wouldn't be able to find anything if you were searching for some important text.</p>

<p><b>Code</b></p>

Finally, here's the complete code, starting with a simple usage example:

<code><pre><b style="color:#000080">import</b> java.io.File;
<b style="color:#000080">import</b> java.io.IOException;
<b style="color:#000080">import</b> java.util.logging.FileHandler;
<b style="color:#000080">import</b> java.util.logging.Level;
<b style="color:#000080">import</b> java.util.logging.Logger;

<b style="color:#000080"><b style="color:#000080">public</b> class</b> <b>Main</b> {
    <b style="color:#000080"><b style="color:#000080">public</b> static void</b> main(String[] args) throws IOException {
        String userHome    = System.getProperty(<b style="font-weight:normal;color:#008000">"user.home"</b>);
        File   logFile     = <b style="color:#000080">new</b> File(userHome, <b style="font-weight:normal;color:#008000">"bigSevereText.log"</b>);
        String logFilePath = logFile.getPath();
        System.out.println(<b style="font-weight:normal;color:#008000">"Logging to file "</b> + logFilePath);    

        Logger logger = Logger.getLogger(<b style="font-weight:normal;color:#008000">"default"</b>);
        logger.setLevel(Level.FINEST);
        FileHandler logFileHandler = <b style="color:#000080">new</b> FileHandler(logFilePath);
        logFileHandler.setFormatter(<b style="color:#000080">new</b> AsciiArtFormatter());
        logger.addHandler(logFileHandler);

        logger.fine(<b style="font-weight:normal;color:#008000">"Fox prepares to jump."</b>);
        logger.info(<b style="font-weight:normal;color:#008000">"Fox is ready to jump."</b>);
        logger.severe(<b style="font-weight:normal;color:#008000">"Fox failed\nto jump over\nlazy dog."</b>);
        logger.info(<b style="font-weight:normal;color:#008000">"Dog is chasing fox."</b>);
    }
}

</pre></code>
<p>I have made the <code>getAsciiArt()</code> method part of the formatter, but you might like to move it to some
other place (it is totally independent from logging after all):</p>

<code><pre><b style="color:#000080">import</b> java.util.logging.Formatter;
<b style="color:#000080">import</b> java.util.logging.LogRecord;
<b style="color:#000080">import</b> java.util.logging.SimpleFormatter;
<b style="color:#000080">import</b> java.util.logging.Level;
<b style="color:#000080">import</b> java.util.Arrays;
<b style="color:#000080">import</b> java.awt.*;
<b style="color:#000080">import</b> java.awt.image.BufferedImage;
<b style="color:#000080">import</b> java.awt.image.Raster;
<b style="color:#000080">import</b> java.awt.geom.Rectangle2D;
<b style="color:#000080">import</b> java.awt.font.FontRenderContext;
<b style="color:#000080">import</b> java.awt.font.GlyphVector;
<b style="color:#000080">import</b> java.awt.font.TextLayout;

<b style="color:#000080">public class</b> <b>AsciiArtFormatter</b> <b style="color:#000080">extends</b> Formatter {
    <b style="color:#000080"><b style="color:#000080">private</b> final</b> Formatter formatter;
    <b style="color:#000080"><b style="color:#000080">private</b> final</b> Font      font;
    <b style="color:#000080"><b style="color:#000080">private</b> final</b> Level     level;

    <b style="color:#000080">public</b> AsciiArtFormatter() {
        <b style="color:#000080">this</b>(<b style="color:#000080">null</b>);
    }

    <b style="color:#000080">public</b> AsciiArtFormatter(Formatter formatter) {
        <b style="color:#000080">this</b>(formatter, <b style="color:#000080">null</b>, <b style="color:#000080">null</b>);
    }

    <b style="font-weight:normal;color:#800000">/**
     * Creates a formatter that will additionally use ascii art to log the message in the
     * given <code>font</code> if its log level is at least <code>level</code>.
     *
     * <b>@param</b> formatter used to format the message before appending ascii art message.
     *                  If null a default <code>SimpleFormatter</code> is used.
     * <b>@param</b> level     minimum level needed to enable ascii art logging.
     *                  If null <code>Level.SEVERE</code> is used.
     * <b>@param</b> font      font used for any ascii art output.
     *                  If null a sans serif font of size 18 is used.
     */</b>
    <b style="color:#000080">public</b> AsciiArtFormatter(Formatter formatter, Level level, Font font) {
        <b style="color:#000080">this</b>.formatter = (formatter == <b style="color:#000080">null</b> ? <b style="color:#000080">new</b> SimpleFormatter() : formatter);
        <b style="color:#000080">this</b>.level     = level == <b style="color:#000080">null</b> ? Level.SEVERE : level;
        <b style="color:#000080">this</b>.font      = font == <b style="color:#000080">null</b> ? <b style="color:#000080">new</b> Font(<b style="font-weight:normal;color:#008000">"SansSerif"</b>, Font.PLAIN, <b style="font-weight:normal;color:#0000ff">18</b>) : font;
    }

    <b style="color:#000080">public</b> String format(LogRecord record) {
        String message = formatter.format(record);
        <b style="color:#000080">if</b>(record.getLevel().intValue() >= level.intValue()) {
            message = message + getAsciiArt(font, record.getMessage());
        }
        <b style="color:#000080">return</b> message;
    }

    <b style="font-weight:normal;color:#800000">/**
     * <b>@return</b> the text as 'Ascii Art' in the given font using a '#' character for
     *         each pixel (the text itself if any runtime exception occurs)  
     */</b>
    <b style="color:#000080">public</b> static String getAsciiArt(Font font, String text) {
        <b style="color:#000080">try</b> {
            <b style="color:#000080">if</b>(text == <b style="color:#000080">null</b>) {
                text = "(<b style="color:#000080">null</b>)";
            }
            <b style="color:#000080">int</b> tabWidth = <b style="font-weight:normal;color:#0000ff">4</b>;
            
            text = text.replaceAll(<b style="font-weight:normal;color:#008000">"\\t"</b>, repeat(<b style="font-weight:normal;color:#008000">' '</b>, tabWidth));
            FontRenderContext fontRenderContext = <b style="color:#000080">new</b> FontRenderContext(<b style="color:#000080">null</b>, false, false);
            StringBuffer      buffer            = <b style="color:#000080">new</b> StringBuffer();
            String[]          lines             = text.split(<b style="font-weight:normal;color:#008000">"\n|\r\n"</b>, <b style="font-weight:normal;color:#0000ff">0</b>);
            <b style="color:#000080">for</b>(<b style="color:#000080">int</b> lineIndex = <b style="font-weight:normal;color:#0000ff">0</b>; lineIndex < lines.length; lineIndex++) {
                <b style="font-weight:normal;color:#800000">// How stupid: TextLayout does not handle white space like I want to, and
                // GlyphVector has no convenient method to calculate overall ascent/descent.
                // So just use both...</b>
    
                String      line        = lines[lineIndex];
                GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
                Rectangle2D bounds      = glyphVector.getLogicalBounds();
                <b style="color:#000080">int</b>         width       = (<b style="color:#000080">int</b>) bounds.getWidth();
                <b style="color:#000080">int</b>         height      = (<b style="color:#000080">int</b>) bounds.getHeight();

                <b style="color:#000080">if</b>(line.length() == <b style="font-weight:normal;color:#0000ff">0</b>) {
                    buffer.append(repeat(<b style="font-weight:normal;color:#008000">'\n'</b>, height));
                    continue;
                }

                TextLayout    textLayout = <b style="color:#000080">new</b> TextLayout(line, font, fontRenderContext);
                <b style="color:#000080">float</b>         ascent     = textLayout.getAscent();
                BufferedImage image      = <b style="color:#000080">new</b> BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
                Graphics2D    graphics   = image.createGraphics();
                
                <b style="color:#000080">try</b> {
                    graphics.drawGlyphVector(glyphVector, <b style="font-weight:normal;color:#0000ff">0</b>, ascent);

                    Raster raster = image.getRaster();
                    <b style="color:#000080">int</b>[] ints = <b style="color:#000080">new</b> <b style="color:#000080">int</b>[1];
                    <b style="color:#000080">for</b>(<b style="color:#000080">int</b> y = <b style="font-weight:normal;color:#0000ff">0</b>; y < height; ++y) {
                        <b style="color:#000080">for</b>(<b style="color:#000080">int</b> x = <b style="font-weight:normal;color:#0000ff">0</b>; x < width; ++x) {
                            raster.getPixel(x, y, ints);
                            <b style="color:#000080">int</b> pixelValue = ints[<b style="font-weight:normal;color:#0000ff">0</b>];
                            buffer.append(pixelValue > <b style="font-weight:normal;color:#0000ff">0</b> ? <b style="font-weight:normal;color:#008000">'#'</b> : <b style="font-weight:normal;color:#008000">' '</b>);
                        }
                        buffer.append(<b style="font-weight:normal;color:#008000">'\n'</b>);
                    }
                }
                finally {
                    graphics.dispose();
                }

            }

            <b style="color:#000080">return</b> <b style="color:#000080">new</b> String(buffer);
        }
        <b style="color:#000080">catch</b>(RuntimeException e) {
            <b style="font-weight:normal;color:#800000">// Don't want anything silly to happen only because using this fun method.
            // Revert to plain output ;-(</b>
            <b style="color:#000080">return</b> text;
        }
    }
    
    <b style="font-weight:normal;color:#800000">/**
     * <b>@return</b> a string consisting of the character <code>c</code> repeated <code>count</code> times.
     */</b>
    <b style="color:#000080">public</b> static String repeat(char c, <b style="color:#000080">int</b> count) {
        char[] fill = <b style="color:#000080">new</b> char[count];
        Arrays.fill(fill, c);
        <b style="color:#000080">return</b> <b style="color:#000080">new</b> String(fill);
    }
}
</pre></code>]]>

</content>
</entry>

</feed>