View Javadoc

1   /*
2    *  XNap Commons
3    *
4    *  Copyright (C) 2005  Steffen Pingel
5    *  Copyright (C) 2005  Felix Berger
6    *
7    *  This library is free software; you can redistribute it and/or
8    *  modify it under the terms of the GNU Lesser General Public
9    *  License as published by the Free Software Foundation; either
10   *  version 2.1 of the License, or (at your option) any later version.
11   *
12   *  This library is distributed in the hope that it will be useful,
13   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   *  Lesser General Public License for more details.
16   *
17   *  You should have received a copy of the GNU Lesser General Public
18   *  License along with this library; if not, write to the Free Software
19   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20   */
21  package org.xnap.commons.gui.shortcut;
22  
23  import java.awt.event.ActionEvent;
24  import java.awt.event.InputEvent;
25  import java.awt.event.KeyEvent;
26  import java.util.Collections;
27  import java.util.Iterator;
28  import java.util.LinkedList;
29  import java.util.List;
30  import javax.swing.Action;
31  import javax.swing.JEditorPane;
32  import javax.swing.JTextArea;
33  import javax.swing.JTextField;
34  import javax.swing.JTextPane;
35  import javax.swing.KeyStroke;
36  import javax.swing.text.BadLocationException;
37  import javax.swing.text.DefaultEditorKit;
38  import javax.swing.text.Document;
39  import javax.swing.text.JTextComponent;
40  import javax.swing.text.Keymap;
41  import javax.swing.text.TextAction;
42  import javax.swing.text.Utilities;
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  /***
47   * Generic class which activates Emacs keybindings for java input {@link
48   * JTextComponent}s.
49   * 
50   * The inner class actions can also be used independently.
51   *
52   * @author Felix Berger
53   */
54  public class EmacsKeyBindings
55  {
56  
57      public static final String killLineAction = "emacs-kill-line";
58  
59      public static final String killRingSaveAction = "emacs-kill-ring-save";
60  
61      public static final String killRegionAction = "emacs-kill-region";
62  
63      public static final String backwardKillWordAction 
64  		= "emacs-backward-kill-word";
65  
66      public static final String capitalizeWordAction = "emacs-capitalize-word";
67  
68      public static final String downcaseWordAction = "emacs-downcase-word";
69      
70      public static final String killWordAction = "emacs-kill-word";
71  
72      public static final String setMarkCommandAction 
73  		= "emacs-set-mark-command";
74  
75      public static final String yankAction = "emacs-yank";
76  
77      public static final String yankPopAction = "emacs-yank-pop";
78  
79      public static final String upcaseWordAction = "emacs-upcase-word";
80  
81      public static final JTextComponent.KeyBinding[] EMACS_KEY_BINDINGS = {
82  		new JTextComponent.
83  			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
84  											  InputEvent.CTRL_MASK),
85  					   DefaultEditorKit.pasteAction),
86  		new JTextComponent.
87  			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
88  											  InputEvent.ALT_MASK),
89  					   DefaultEditorKit.copyAction),
90  		new JTextComponent.
91  			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
92  											  InputEvent.CTRL_MASK),
93  					   DefaultEditorKit.cutAction),
94  		new JTextComponent.
95  			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_E,
96  											  InputEvent.CTRL_MASK),
97  					   DefaultEditorKit.endLineAction),
98  		new JTextComponent.
99  			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_A,
100 											  InputEvent.CTRL_MASK),
101 					   DefaultEditorKit.beginLineAction),
102 		new JTextComponent.
103 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_D,
104 											  InputEvent.CTRL_MASK),
105 					   DefaultEditorKit.deleteNextCharAction),
106 		new JTextComponent.
107 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_N,
108 											  InputEvent.CTRL_MASK),
109 					   DefaultEditorKit.downAction),
110 		new JTextComponent.
111 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_P,
112 											  InputEvent.CTRL_MASK),
113 					   DefaultEditorKit.upAction),
114 		new JTextComponent.
115 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_B,
116 											  InputEvent.ALT_MASK),
117 					   DefaultEditorKit.previousWordAction),
118 		new JTextComponent.
119 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
120 											  InputEvent.ALT_MASK),
121 					   DefaultEditorKit.beginAction),
122 		new JTextComponent.
123 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
124 											  InputEvent.ALT_MASK 
125 											  + InputEvent.SHIFT_MASK),
126 					   DefaultEditorKit.endAction),
127 		new JTextComponent.
128 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F,
129 											  InputEvent.ALT_MASK),
130 					   DefaultEditorKit.nextWordAction),
131 		new JTextComponent.
132 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F,
133 											  InputEvent.CTRL_MASK),
134 					   DefaultEditorKit.forwardAction),
135 		new JTextComponent.
136 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_B,
137 											  InputEvent.CTRL_MASK),
138 					   DefaultEditorKit.backwardAction),
139 		new JTextComponent.
140 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V,
141 											  InputEvent.CTRL_MASK),
142 					   DefaultEditorKit.pageDownAction),
143 		new JTextComponent.
144 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V,
145 											  InputEvent.ALT_MASK),
146 					   DefaultEditorKit.pageUpAction),
147 		new JTextComponent.
148 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_D,
149 											  InputEvent.ALT_MASK),
150 					   EmacsKeyBindings.killWordAction),
151 		new JTextComponent.
152 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,
153 											  InputEvent.ALT_MASK),
154 					   EmacsKeyBindings.backwardKillWordAction),
155 		new JTextComponent.
156 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
157 											  InputEvent.CTRL_MASK),
158 					   EmacsKeyBindings.setMarkCommandAction),
159 		new JTextComponent.
160 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
161 											  InputEvent.ALT_MASK),
162 					   EmacsKeyBindings.killRingSaveAction),
163 		new JTextComponent.
164 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
165 											  InputEvent.CTRL_MASK),
166 					   EmacsKeyBindings.killRegionAction),
167 	
168 		new JTextComponent.
169 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_K,
170 											  InputEvent.CTRL_MASK),
171 					   EmacsKeyBindings.killLineAction),
172 	
173 		new JTextComponent.
174 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
175 											  InputEvent.CTRL_MASK),
176 					   EmacsKeyBindings.yankAction),
177 
178 		new JTextComponent.
179 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
180 											  InputEvent.ALT_MASK),
181 					   EmacsKeyBindings.yankPopAction),
182 	
183 		new JTextComponent.
184 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_C,
185 											  InputEvent.ALT_MASK),
186 					   EmacsKeyBindings.capitalizeWordAction),
187 	    
188 		new JTextComponent.
189 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_L,
190 											  InputEvent.ALT_MASK),
191 					   EmacsKeyBindings.downcaseWordAction),
192 	    
193 		new JTextComponent.
194 			KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_U,
195 											  InputEvent.ALT_MASK),
196 					   EmacsKeyBindings.upcaseWordAction),
197     };
198 
199 
200 	private static Log logger = LogFactory.getLog(EmacsKeyBindings.class);
201 
202 	/***
203 	 * Loads the emacs keybindings for all common <code>JTextComponent</code>s.
204 	 * 
205 	 * The shared keymap instances of the concrete subclasses of 
206 	 * {@link JTextComponent} are fed with the keybindings.
207 	 *
208 	 * The original keybindings are stored in a backup array.
209 	 */
210 	public static void load()
211 	{
212 		JTextComponent[] jtcs = new JTextComponent[] {
213 			new JTextArea(),
214 			new JTextPane(),
215 			new JTextField(),
216 			new JEditorPane(),
217 		};
218 
219 		for (int i = 0; i < jtcs.length; i++) {
220 			Keymap orig = jtcs[i].getKeymap();
221 			Keymap backup = JTextComponent.addKeymap
222 				(jtcs[i].getClass().getName(), null);
223 			
224 			Action[] bound = orig.getBoundActions();
225 			for (int j = 0; j < bound.length; j++) {
226 				KeyStroke[] strokes = orig.getKeyStrokesForAction(bound[j]);
227 				for (int k = 0; k < strokes.length; k++) {
228 					backup.addActionForKeyStroke(strokes[k], bound[j]);
229 				}
230 			}
231 
232 			backup.setDefaultAction(orig.getDefaultAction());
233 		}
234 
235 		loadEmacsKeyBindings();
236 	}
237 
238 	/***
239 	 * Restores the original keybindings for the concrete subclasses of
240 	 * {@link JTextComponent}.
241 	 *
242 	 */
243 	public static void unload()
244 	{
245 		JTextComponent[] jtcs = new JTextComponent[] {
246 			new JTextArea(),
247 			new JTextPane(),
248 			new JTextField(),
249 			new JEditorPane(),
250 		};
251 
252 		for (int i = 0; i < jtcs.length; i++) {
253 			Keymap backup = JTextComponent.getKeymap
254 				(jtcs[i].getClass().getName());
255 
256 			if (backup != null) {
257 				Keymap current = jtcs[i].getKeymap();
258 				current.removeBindings();
259 
260 				Action[] bound = backup.getBoundActions();
261 				for (int j = 0; j < bound.length; j++) {
262 					KeyStroke[] strokes = 
263 						backup.getKeyStrokesForAction(bound[i]);
264 					for (int k = 0; k < strokes.length; k++) {
265 						current.addActionForKeyStroke(strokes[k], bound[j]);
266 					}
267 				}
268 				current.setDefaultAction(backup.getDefaultAction());
269 			}
270 		}
271 	}
272 
273     /***
274      * Activates Emacs keybindings for all text components extending {@link
275      * JTextComponent}.
276      */
277     private static void loadEmacsKeyBindings()
278     {
279 		logger.debug("Loading emacs keybindings");
280 
281 		JTextComponent[] jtcs = new JTextComponent[] {
282 			new JTextArea(),
283 			new JTextPane(),
284 			new JTextField(),
285 			new JEditorPane(),
286 		};
287 
288 		for (int i = 0; i < jtcs.length; i++) {
289 	
290 			Keymap k = jtcs[i].getKeymap();
291 
292 			JTextComponent.loadKeymap(k, EMACS_KEY_BINDINGS,
293 									  jtcs[i].getActions());
294 	    
295 			k.addActionForKeyStroke(KeyStroke.getKeyStroke
296 									(KeyEvent.VK_D, InputEvent.ALT_MASK),
297 									new KillWordAction(killWordAction));
298 			k.addActionForKeyStroke(KeyStroke.getKeyStroke
299 									(KeyEvent.VK_BACK_SPACE, 
300 									 InputEvent.ALT_MASK),
301 									new BackwardKillWordAction(backwardKillWordAction));
302 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
303 														   InputEvent.CTRL_MASK),
304 									new SetMarkCommandAction(setMarkCommandAction));
305 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_W,
306 														   InputEvent.ALT_MASK),
307 									new KillRingSaveAction(killRingSaveAction));
308 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_W,
309 														   InputEvent.CTRL_MASK),
310 									new KillRegionAction(killRegionAction));
311 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_K,
312 														   InputEvent.CTRL_MASK),
313 									new KillLineAction(killLineAction));
314 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
315 														   InputEvent.CTRL_MASK),
316 									new YankAction("emacs-yank"));
317 			k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
318 														   InputEvent.ALT_MASK),
319 									new YankPopAction("emacs-yank-pop"));
320 			k.addActionForKeyStroke
321 				(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.ALT_MASK),
322 				 new CapitalizeWordAction(capitalizeWordAction));
323 	    
324 			k.addActionForKeyStroke
325 				(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.ALT_MASK),
326 				 new DowncaseWordAction(downcaseWordAction));
327 
328 			k.addActionForKeyStroke
329 				(KeyStroke.getKeyStroke(KeyEvent.VK_U, InputEvent.ALT_MASK),
330 				 new UpcaseWordAction(upcaseWordAction));
331 		}
332     }
333 
334     /***
335      * This action kills the next word.
336      * 
337      * It removes the next word on the right side of the cursor from the active
338      * text component and adds it to the clipboard. 
339      */
340     public static class KillWordAction extends TextAction
341     {
342 		public KillWordAction(String nm)
343 		{
344 			super(nm);
345 		}
346 
347 		public void actionPerformed(ActionEvent e)
348 		{
349 			JTextComponent jtc = getTextComponent(e);
350 			if (jtc != null) {
351 				try {
352 					int offs = jtc.getCaretPosition();
353 					jtc.setSelectionStart(offs);
354 					offs = getWordEnd(jtc, offs);
355 					jtc.setSelectionEnd(offs);
356 					KillRing.getInstance().add(jtc.getSelectedText());
357 					jtc.cut();
358 				}
359 				catch (BadLocationException ble) {
360 					jtc.getToolkit().beep();
361 				}
362 			}
363 		}
364     }
365 
366     /***
367      * This action kills the previous word.
368      * 
369      * It removes the previous word on the left side of the cursor from the 
370      * active text component and adds it to the clipboard.
371      */
372     public static class BackwardKillWordAction extends TextAction
373     {
374 		public BackwardKillWordAction(String nm)
375 		{
376 			super(nm);
377 		}
378 
379 		public void actionPerformed(ActionEvent e)
380 		{
381 			JTextComponent jtc = getTextComponent(e);
382 			if (jtc != null) {
383 				try {
384 					int offs = jtc.getCaretPosition();
385 					jtc.setSelectionEnd(offs);
386 					offs = Utilities.getPreviousWord(jtc, offs);
387 					jtc.setSelectionStart(offs);
388 					KillRing.getInstance().add(jtc.getSelectedText());
389 					jtc.cut();
390 				}
391 				catch (BadLocationException ble) {
392 					jtc.getToolkit().beep();
393 				}
394 			}
395 		}
396     }
397 
398     /***
399      * This action copies the marked region and stores it in the killring.
400      */
401     public static class KillRingSaveAction extends TextAction
402     {
403 		public KillRingSaveAction(String nm)
404 		{
405 			super(nm);
406 		}
407 
408 		public void actionPerformed(ActionEvent e)
409 		{
410 			JTextComponent jtc = getTextComponent(e);
411 			if (jtc != null && SetMarkCommandAction.isMarked(jtc)) {
412 				jtc.setSelectionStart(SetMarkCommandAction.getCaretPosition());
413 				jtc.moveCaretPosition(jtc.getCaretPosition());
414 				jtc.copy();
415 				// TODO is this correct?
416 				KillRing.getInstance().add(jtc.getSelectedText());
417 				SetMarkCommandAction.reset();
418 				// todo reset selection
419 			}
420 		}
421     }
422 
423     /***
424      * This action Kills the marked region and stores it in the killring.
425      */
426     public static class KillRegionAction extends TextAction
427     {
428 		public KillRegionAction(String nm)
429 		{
430 			super(nm);
431 		}
432 
433 		public void actionPerformed(ActionEvent e)
434 		{
435 			JTextComponent jtc = getTextComponent(e);
436 			if (jtc != null && SetMarkCommandAction.isMarked(jtc)) {
437 				int start, end;
438 				if (SetMarkCommandAction.getCaretPosition() < jtc.getCaretPosition()) {
439 					start = SetMarkCommandAction.getCaretPosition();
440 					end = jtc.getCaretPosition();
441 				}
442 				else {
443 					start = jtc.getCaretPosition();
444 					end = SetMarkCommandAction.getCaretPosition();
445 				}
446 				if (start != end) {
447 					jtc.select(start, end);
448 					SetMarkCommandAction.reset();
449 					KillRing.getInstance().add(jtc.getSelectedText());
450 					jtc.cut();
451 				}
452 				else {
453 					jtc.getToolkit().beep();
454 				}
455 			}
456 		}
457     }
458 
459     /***
460      * This actin kills text up to the end of the current line and stores it in 
461      * the killring.
462      */
463     public static class KillLineAction extends TextAction
464     {
465 		public KillLineAction(String nm)
466 		{
467 			super(nm);
468 		}
469 
470 		public void actionPerformed(ActionEvent e)
471 		{
472 			JTextComponent jtc = getTextComponent(e);
473 			if (jtc != null) {
474 				try {
475 					int start = jtc.getCaretPosition();
476 					int end = Utilities.getRowEnd(jtc, start);
477 					if (start == end && jtc.isEditable()) {
478 						Document doc = jtc.getDocument();
479 						doc.remove(end, 1);
480 					}
481 					else {
482 						jtc.setSelectionStart(start);
483 						jtc.setSelectionEnd(end);
484 						KillRing.getInstance().add(jtc.getSelectedText());
485 						jtc.cut();
486 						// jtc.replaceSelection("");
487 					}
488 				}
489 				catch (BadLocationException ble) {
490 					jtc.getToolkit().beep();
491 				}
492 			}
493 		}
494     }
495 
496     /***
497      * This action sets a beginning mark for a selection.
498      */
499     public static class SetMarkCommandAction extends TextAction
500     {
501 		private static int position = -1;
502 		private static JTextComponent jtc;
503 	
504 		public SetMarkCommandAction(String nm)
505 		{
506 			super(nm);
507 		}
508 	
509 		public void actionPerformed(ActionEvent e)
510 		{
511 			jtc = getTextComponent(e);
512 			if (jtc != null) {
513 				position = jtc.getCaretPosition();
514 			}
515 		}
516 
517 		public static boolean isMarked(JTextComponent jt)
518 		{
519 			return (jtc == jt && position != -1);
520 		}
521 
522 		public static void reset()
523 		{
524 			jtc = null;
525 			position = -1;
526 		}
527 
528 		public static int getCaretPosition()
529 		{
530 			return position;
531 		}
532     }
533 
534     /***
535      * This action pastes text from the killring.
536      */
537     public static class YankAction extends TextAction
538     {
539 		public static int start = -1;
540 		public static int end = -1;
541 	
542 		public YankAction(String nm)
543 		{
544 			super(nm);
545 		}
546 	
547 		public void actionPerformed(ActionEvent event)
548 		{
549 			JTextComponent jtc = getTextComponent(event);
550 	    
551 			if (jtc != null) {
552 				try {
553 					start = jtc.getCaretPosition();
554 					jtc.paste();
555 					end = jtc.getCaretPosition();
556 					KillRing.getInstance().add(jtc.getText(start, end));
557 					KillRing.getInstance().setCurrentTextComponent(jtc);
558 				}
559 				catch (Exception e) {
560 				}
561 			}
562 		}
563     }
564 	
565     /***
566      * This action pastes an element from the killring cycling through it.
567      */
568     public static class YankPopAction extends TextAction
569     {
570 
571 		public YankPopAction(String nm)
572 		{
573 			super(nm);
574 		}
575 
576 		public void actionPerformed(ActionEvent event)
577 		{
578 			JTextComponent jtc = getTextComponent(event);
579 	    
580 			if (jtc != null 
581 					&& KillRing.getInstance().getCurrentTextComponent() == jtc
582 					&& jtc.getCaretPosition() == YankAction.end
583 					&& !KillRing.getInstance().isEmpty()) {
584 				jtc.setSelectionStart(YankAction.start);
585 				jtc.setSelectionEnd(YankAction.end);
586 				String toYank = KillRing.getInstance().next();
587 				if (toYank != null) {
588 					jtc.replaceSelection(toYank);
589 					YankAction.end = jtc.getCaretPosition();
590 				}
591 				else {
592 					jtc.getToolkit().beep();
593 				}
594 			}
595 		}
596     }
597     
598     /***
599      * Manages all killed (cut) text pieces in a ring which is accessible
600      * through {@link YankPopAction}.
601      * <p>
602      * Also provides an unmodifiable copy of all cut pieces. 
603      */
604     public static class KillRing
605     {
606     	private JTextComponent jtc;
607     	private LinkedList<String> ring = new LinkedList<String>();
608     	Iterator<String> iter = ring.iterator();
609     	
610     	private static final KillRing instance = new KillRing();
611     	
612     	public static KillRing getInstance()
613     	{
614     		return instance;
615     	}
616     	
617     	void setCurrentTextComponent(JTextComponent jtc)
618     	{
619     		this.jtc = jtc;
620     	}
621     	
622     	JTextComponent getCurrentTextComponent()
623     	{
624     		return jtc;
625     	}
626     	
627     	/***
628     	 * Adds text to the front of the kill ring.
629     	 * <p>
630     	 * Deviating from the Emacs implementation we make sure the 
631     	 * exact same text is not somewhere else in the ring.
632     	 */
633     	void add(String text)
634     	{
635     		if (text.length() == 0) {
636     			return;
637     		}
638     		
639     		ring.remove(text);
640     		ring.addFirst(text);
641     		while (ring.size() > 60) {
642     			ring.removeLast();
643     		}
644     		iter = ring.iterator();
645     		// skip first entry, the one we just added
646     		iter.next();
647     	}
648     	
649     	/***
650     	 * Returns an unmodifiable version of the ring list which contains
651     	 * the killed texts.
652     	 * @return the content of the kill ring
653     	 */
654     	public List<String> getRing()
655     	{
656     		return Collections.unmodifiableList(ring);
657     	}
658     	
659     	public boolean isEmpty()
660     	{
661     		return ring.isEmpty();
662     	}
663 
664     	/***
665     	 * Returns the next text element which is to be yank-popped.
666     	 * @return <code>null</code> if the ring is empty
667     	 */
668     	String next()
669     	{
670     		if (ring.isEmpty()) {
671     			return null;
672     		}
673     		else if (iter.hasNext()) {
674     			return iter.next();
675     		}
676     		else {
677     			iter = ring.iterator();
678     			// guaranteed to not throw an exception, since ring is not empty
679     			return iter.next();
680     		}
681     	}
682     }
683 
684     /***
685      * This action capitalizes the next word on the right side of the caret.
686      */
687     public static class CapitalizeWordAction extends TextAction
688     {
689 		public CapitalizeWordAction(String nm)
690 		{
691 			super(nm);
692 		}
693 
694 		/***
695 		 * At first the same code as in {@link
696 		 * EmacsKeyBindings.DowncaseWordAction} is performed, to ensure the
697 		 * word is in lower case, then the first letter is capialized.
698 		 */
699 		public void actionPerformed(ActionEvent event)
700 		{
701 			JTextComponent jtc = getTextComponent(event);
702 
703 			if (jtc != null) {
704 				try {
705 					/* downcase code */
706 					int start = jtc.getCaretPosition();
707 					int end = getWordEnd(jtc, start);
708 					jtc.setSelectionStart(start);
709 					jtc.setSelectionEnd(end);
710 					String word = jtc.getText(start, end - start);
711 					jtc.replaceSelection(word.toLowerCase());
712 
713 					/* actual capitalize code */
714 					int offs = Utilities.getWordStart(jtc, start);
715 					// get first letter
716 					String c = jtc.getText(offs, 1);
717 					// we're at the end of the previous word
718 					if (c.equals(" ")) {
719 						/* ugly java workaround to get the beginning of the
720                            word.  */
721 						offs = Utilities.getWordStart(jtc, ++offs);
722 						c = jtc.getText(offs, 1);
723 					}
724 					if (Character.isLetter(c.charAt(0))) {
725 						jtc.setSelectionStart(offs);
726 						jtc.setSelectionEnd(offs + 1);
727 						jtc.replaceSelection(c.toUpperCase());
728 					}
729 					end = Utilities.getWordEnd(jtc, offs);
730 					jtc.setCaretPosition(end);
731 				}
732 				catch (BadLocationException ble) {
733 					jtc.getToolkit().beep();
734 				}
735 			}
736 		}
737     }
738 
739     /***
740      * This action renders all characters of the next word to lowercase.
741      */
742     public static class DowncaseWordAction extends TextAction
743     {
744 		public DowncaseWordAction(String nm)
745 		{
746 			super(nm);
747 		}
748 
749 		public void actionPerformed(ActionEvent event)
750 		{
751 			JTextComponent jtc = getTextComponent(event);
752 
753 			if (jtc != null) {
754 				try {
755 					int start = jtc.getCaretPosition();
756 					int end = getWordEnd(jtc, start);
757 					jtc.setSelectionStart(start);
758 					jtc.setSelectionEnd(end);
759 					String word = jtc.getText(start, end - start);
760 					jtc.replaceSelection(word.toLowerCase());
761 					jtc.setCaretPosition(end);
762 				}
763 				catch (BadLocationException ble) {
764 					jtc.getToolkit().beep();
765 				}
766 			}
767 		}
768     }
769 
770     /***
771      * This action renders all characters of the next word to upppercase.
772      */
773     public static class UpcaseWordAction extends TextAction
774     {
775 		public UpcaseWordAction(String nm)
776 		{
777 			super(nm);
778 		}
779 
780 		public void actionPerformed(ActionEvent event)
781 		{
782 			JTextComponent jtc = getTextComponent(event);
783 
784 			if (jtc != null) {
785 				try {
786 					int start = jtc.getCaretPosition();
787 					int end = getWordEnd(jtc, start);
788 					jtc.setSelectionStart(start);
789 					jtc.setSelectionEnd(end);
790 					String word = jtc.getText(start, end - start);
791 					jtc.replaceSelection(word.toUpperCase());
792 					jtc.setCaretPosition(end);
793 				}
794 				catch (BadLocationException ble) {
795 					jtc.getToolkit().beep();
796 				}
797 			}
798 		}
799     }
800     
801     private static int getWordEnd(JTextComponent jtc, int start)
802     	throws BadLocationException
803     {
804     	try {
805     		return Utilities.getNextWord(jtc, start);
806     	}
807     	catch (BadLocationException ble) {
808     		int end = jtc.getText().length();
809 			if (start < end) {
810 				return end;
811 			}
812 			else {
813 				throw ble;
814 			}
815     	}
816     }
817 }