1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.gui.util;
21
22 import java.awt.event.ActionEvent;
23 import java.awt.event.InputEvent;
24 import java.awt.event.KeyEvent;
25 import java.util.Iterator;
26 import java.util.LinkedList;
27
28 import javax.swing.Action;
29 import javax.swing.JEditorPane;
30 import javax.swing.JTextArea;
31 import javax.swing.JTextField;
32 import javax.swing.JTextPane;
33 import javax.swing.KeyStroke;
34 import javax.swing.text.BadLocationException;
35 import javax.swing.text.DefaultEditorKit;
36 import javax.swing.text.Document;
37 import javax.swing.text.JTextComponent;
38 import javax.swing.text.Keymap;
39 import javax.swing.text.TextAction;
40 import javax.swing.text.Utilities;
41
42 import org.apache.log4j.Logger;
43
44 /***
45 * Generic class which activates Emacs keybindings for java input {@link
46 * JTextComponent}s.
47 *
48 * TODO: Some actions don't work at the end of line, that's because they
49 * depend on built-in actions.
50 */
51 public class EmacsKeyBindings
52 {
53
54
55 public static final String killLineAction = "emacs-kill-line";
56
57 public static final String killRingSaveAction = "emacs-kill-ring-save";
58
59 public static final String killRegionAction = "emacs-kill-region";
60
61 public static final String backwardKillWordAction
62 = "emacs-backward-kill-word";
63
64 public static final String capitalizeWordAction = "emacs-capitalize-word";
65
66 public static final String downcaseWordAction = "emacs-downcase-word";
67
68 public static final String killWordAction = "emacs-kill-word";
69
70 public static final String setMarkCommandAction
71 = "emacs-set-mark-command";
72
73 public static final String yankAction = "emacs-yank";
74
75 public static final String yankPopAction = "emacs-yank-pop";
76
77 public static final String upcaseWordAction = "emacs-upcase-word";
78
79 public static final JTextComponent.KeyBinding[] EMACS_KEY_BINDINGS = {
80 new JTextComponent.
81 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
82 InputEvent.CTRL_MASK),
83 DefaultEditorKit.pasteAction),
84 new JTextComponent.
85 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
86 InputEvent.ALT_MASK),
87 DefaultEditorKit.copyAction),
88 new JTextComponent.
89 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
90 InputEvent.CTRL_MASK),
91 DefaultEditorKit.cutAction),
92 new JTextComponent.
93 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_E,
94 InputEvent.CTRL_MASK),
95 DefaultEditorKit.endLineAction),
96 new JTextComponent.
97 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_A,
98 InputEvent.CTRL_MASK),
99 DefaultEditorKit.beginLineAction),
100 new JTextComponent.
101 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_D,
102 InputEvent.CTRL_MASK),
103 DefaultEditorKit.deleteNextCharAction),
104 new JTextComponent.
105 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_N,
106 InputEvent.CTRL_MASK),
107 DefaultEditorKit.downAction),
108 new JTextComponent.
109 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_P,
110 InputEvent.CTRL_MASK),
111 DefaultEditorKit.upAction),
112 new JTextComponent.
113 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_B,
114 InputEvent.ALT_MASK),
115 DefaultEditorKit.previousWordAction),
116 new JTextComponent.
117 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
118 InputEvent.ALT_MASK),
119 DefaultEditorKit.beginAction),
120 new JTextComponent.
121 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
122 InputEvent.ALT_MASK
123 + InputEvent.SHIFT_MASK),
124 DefaultEditorKit.endAction),
125 new JTextComponent.
126 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F,
127 InputEvent.ALT_MASK),
128 DefaultEditorKit.nextWordAction),
129 new JTextComponent.
130 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F,
131 InputEvent.CTRL_MASK),
132 DefaultEditorKit.forwardAction),
133 new JTextComponent.
134 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_B,
135 InputEvent.CTRL_MASK),
136 DefaultEditorKit.backwardAction),
137 new JTextComponent.
138 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V,
139 InputEvent.CTRL_MASK),
140 DefaultEditorKit.pageDownAction),
141 new JTextComponent.
142 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V,
143 InputEvent.ALT_MASK),
144 DefaultEditorKit.pageUpAction),
145 new JTextComponent.
146 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_D,
147 InputEvent.ALT_MASK),
148 EmacsKeyBindings.killWordAction),
149 new JTextComponent.
150 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,
151 InputEvent.ALT_MASK),
152 EmacsKeyBindings.backwardKillWordAction),
153 new JTextComponent.
154 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
155 InputEvent.CTRL_MASK),
156 EmacsKeyBindings.setMarkCommandAction),
157 new JTextComponent.
158 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
159 InputEvent.ALT_MASK),
160 EmacsKeyBindings.killRingSaveAction),
161 new JTextComponent.
162 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_W,
163 InputEvent.CTRL_MASK),
164 EmacsKeyBindings.killRegionAction),
165
166 new JTextComponent.
167 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_K,
168 InputEvent.CTRL_MASK),
169 EmacsKeyBindings.killLineAction),
170
171 new JTextComponent.
172 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
173 InputEvent.CTRL_MASK),
174 EmacsKeyBindings.yankAction),
175
176 new JTextComponent.
177 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
178 InputEvent.ALT_MASK),
179 EmacsKeyBindings.yankPopAction),
180
181 new JTextComponent.
182 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_C,
183 InputEvent.ALT_MASK),
184 EmacsKeyBindings.capitalizeWordAction),
185
186 new JTextComponent.
187 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_L,
188 InputEvent.ALT_MASK),
189 EmacsKeyBindings.downcaseWordAction),
190
191 new JTextComponent.
192 KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_U,
193 InputEvent.ALT_MASK),
194 EmacsKeyBindings.upcaseWordAction),
195 };
196
197
198
199 private static Logger logger = Logger.getLogger(EmacsKeyBindings.class);
200
201
202
203
204
205 public static void load()
206 {
207 JTextComponent[] jtcs = new JTextComponent[] {
208 new JTextArea(),
209 new JTextPane(),
210 new JTextField(),
211 new JEditorPane(),
212 };
213
214 for (int i = 0; i < jtcs.length; i++) {
215 Keymap orig = jtcs[i].getKeymap();
216 Keymap backup = JTextComponent.addKeymap
217 (jtcs[i].getClass().getName(), null);
218
219 Action[] bound = orig.getBoundActions();
220 for (int j = 0; j < bound.length; j++) {
221 KeyStroke[] strokes = orig.getKeyStrokesForAction(bound[j]);
222 for (int k = 0; k < strokes.length; k++) {
223 backup.addActionForKeyStroke(strokes[k], bound[j]);
224 }
225 }
226
227 backup.setDefaultAction(orig.getDefaultAction());
228 }
229
230 loadEmacsKeyBindings();
231 }
232
233 public static void unload()
234 {
235 JTextComponent[] jtcs = new JTextComponent[] {
236 new JTextArea(),
237 new JTextPane(),
238 new JTextField(),
239 new JEditorPane(),
240 };
241
242 for (int i = 0; i < jtcs.length; i++) {
243 Keymap backup = JTextComponent.getKeymap
244 (jtcs[i].getClass().getName());
245
246 if (backup != null) {
247 Keymap current = jtcs[i].getKeymap();
248 current.removeBindings();
249
250 Action[] bound = backup.getBoundActions();
251 for (int j = 0; j < bound.length; j++) {
252 KeyStroke[] strokes =
253 backup.getKeyStrokesForAction(bound[i]);
254 for (int k = 0; k < strokes.length; k++) {
255 current.addActionForKeyStroke(strokes[k], bound[j]);
256 }
257 }
258 current.setDefaultAction(backup.getDefaultAction());
259 }
260 }
261 }
262
263 /***
264 * Activates Emacs keybindings for all text components extending {@link
265 * JTextComponent}.
266 */
267 private static void loadEmacsKeyBindings()
268 {
269 logger.debug("Loading emacs keybindings");
270
271 JTextComponent[] jtcs = new JTextComponent[] {
272 new JTextArea(),
273 new JTextPane(),
274 new JTextField(),
275 new JEditorPane(),
276 };
277
278 for (int i = 0; i < jtcs.length; i++) {
279
280 Keymap k = jtcs[i].getKeymap();
281
282 JTextComponent.loadKeymap(k, EMACS_KEY_BINDINGS,
283 jtcs[i].getActions());
284
285 k.addActionForKeyStroke(KeyStroke.getKeyStroke
286 (KeyEvent.VK_D, InputEvent.ALT_MASK),
287 new KillWordAction(killWordAction));
288 k.addActionForKeyStroke(KeyStroke.getKeyStroke
289 (KeyEvent.VK_BACK_SPACE,
290 InputEvent.ALT_MASK),
291 new BackwardKillWordAction(backwardKillWordAction));
292 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
293 InputEvent.CTRL_MASK),
294 new SetMarkCommandAction(setMarkCommandAction));
295 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_W,
296 InputEvent.ALT_MASK),
297 new KillRingSaveAction(killRingSaveAction));
298 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_W,
299 InputEvent.CTRL_MASK),
300 new KillRegionAction(killRegionAction));
301 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_K,
302 InputEvent.CTRL_MASK),
303 new KillLineAction(killLineAction));
304 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
305 InputEvent.CTRL_MASK),
306 new YankAction("emacs-yank"));
307 k.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
308 InputEvent.ALT_MASK),
309 new YankPopAction("emacs-yank-pop"));
310 k.addActionForKeyStroke
311 (KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.ALT_MASK),
312 new CapitalizeWordAction(capitalizeWordAction));
313
314 k.addActionForKeyStroke
315 (KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.ALT_MASK),
316 new DowncaseWordAction(downcaseWordAction));
317
318 k.addActionForKeyStroke
319 (KeyStroke.getKeyStroke(KeyEvent.VK_U, InputEvent.ALT_MASK),
320 new UpcaseWordAction(upcaseWordAction));
321 }
322 }
323
324
325
326 /***
327 * Kills the next word.
328 */
329 public static class KillWordAction extends TextAction
330 {
331 public KillWordAction(String nm)
332 {
333 super(nm);
334 }
335
336 public void actionPerformed(ActionEvent e)
337 {
338 JTextComponent jtc = getTextComponent(e);
339 if (jtc != null) {
340 try {
341 int offs = jtc.getCaretPosition();
342 jtc.setSelectionStart(offs);
343 offs = Utilities.getNextWord(jtc, offs);
344 jtc.setSelectionEnd(offs);
345 jtc.cut();
346 }
347 catch (BadLocationException ble) {
348 jtc.getToolkit().beep();
349 }
350 }
351 }
352 }
353
354 /***
355 * Kills the previous word.
356 */
357 public static class BackwardKillWordAction extends TextAction
358 {
359 public BackwardKillWordAction(String nm)
360 {
361 super(nm);
362 }
363
364 public void actionPerformed(ActionEvent e)
365 {
366 JTextComponent jtc = getTextComponent(e);
367 if (jtc != null) {
368 try {
369 int offs = jtc.getCaretPosition();
370 jtc.setSelectionEnd(offs);
371 offs = Utilities.getPreviousWord(jtc, offs);
372 jtc.setSelectionStart(offs);
373 jtc.cut();
374 }
375 catch (BadLocationException ble) {
376 jtc.getToolkit().beep();
377 }
378 }
379 }
380 }
381
382 /***
383 * Copies the marked region and stores it in the killring.
384 */
385 public static class KillRingSaveAction extends TextAction
386 {
387 public KillRingSaveAction(String nm)
388 {
389 super(nm);
390 }
391
392 public void actionPerformed(ActionEvent e)
393 {
394 JTextComponent jtc = getTextComponent(e);
395 if (jtc != null && SetMarkCommandAction.isMarked(jtc)) {
396 jtc.setSelectionStart(SetMarkCommandAction.getCaretPosition());
397 jtc.moveCaretPosition(jtc.getCaretPosition());
398 jtc.copy();
399 YankAction.add(jtc.getSelectedText());
400 SetMarkCommandAction.reset();
401
402 }
403 }
404 }
405
406 /***
407 * Kills the marked region and stores it in the killring.
408 */
409 public static class KillRegionAction extends TextAction
410 {
411 public KillRegionAction(String nm)
412 {
413 super(nm);
414 }
415
416 public void actionPerformed(ActionEvent e)
417 {
418 JTextComponent jtc = getTextComponent(e);
419 if (jtc != null && SetMarkCommandAction.isMarked(jtc)) {
420 jtc.setSelectionStart(SetMarkCommandAction.getCaretPosition());
421 jtc.moveCaretPosition(jtc.getCaretPosition());
422 SetMarkCommandAction.reset();
423 YankAction.add(jtc.getSelectedText());
424 jtc.cut();
425 }
426 }
427 }
428
429 /***
430 * Kills text up to the end of the current line and stores it in the
431 * killring.
432 */
433 public static class KillLineAction extends TextAction
434 {
435 public KillLineAction(String nm)
436 {
437 super(nm);
438 }
439
440 public void actionPerformed(ActionEvent e)
441 {
442 JTextComponent jtc = getTextComponent(e);
443 if (jtc != null) {
444 try {
445 int start = jtc.getCaretPosition();
446 int end = Utilities.getRowEnd(jtc, start);
447 if (start == end && jtc.isEditable()) {
448 Document doc = jtc.getDocument();
449 doc.remove(end, 1);
450 }
451 else {
452 jtc.setSelectionStart(start);
453 jtc.setSelectionEnd(end);
454 YankAction.add(jtc.getSelectedText());
455 jtc.cut();
456
457 }
458 }
459 catch (BadLocationException ble) {
460 jtc.getToolkit().beep();
461 }
462 }
463 }
464 }
465
466 /***
467 * Sets a beginning mark.
468 */
469 public static class SetMarkCommandAction extends TextAction
470 {
471
472
473
474
475 private static int position = -1;
476 private static JTextComponent jtc;
477
478
479
480 public SetMarkCommandAction(String nm)
481 {
482 super(nm);
483 }
484
485
486
487 public void actionPerformed(ActionEvent e)
488 {
489 jtc = getTextComponent(e);
490 if (jtc != null) {
491 position = jtc.getCaretPosition();
492 }
493 }
494
495 public static boolean isMarked(JTextComponent jt)
496 {
497 return (jtc == jt && position != -1);
498 }
499
500 public static void reset()
501 {
502 jtc = null;
503 position = -1;
504 }
505
506 public static int getCaretPosition()
507 {
508 return position;
509 }
510 }
511
512 /***
513 * Pastes text from killring.
514 */
515 public static class YankAction extends TextAction
516 {
517 public static LinkedList killring = new LinkedList();
518 public static int start = -1;
519 public static int end = -1;
520
521 public YankAction(String nm)
522 {
523 super(nm);
524 }
525
526 public void actionPerformed(ActionEvent event)
527 {
528 JTextComponent jtc = getTextComponent(event);
529
530 if (jtc != null) {
531 try {
532 start = jtc.getCaretPosition();
533 jtc.paste();
534 end = jtc.getCaretPosition();
535 add(jtc.getText(start, end));
536 }
537 catch (Exception e) {
538 }
539 }
540 }
541
542
543 /***
544 * Uniquely adds <code>item</code> to killring, i.e. if
545 * <code>item</code> is already in killring, it's moved to the front,
546 * otherwise it's added as first element.
547 */
548 public static void add(String item)
549 {
550 for (Iterator i = killring.iterator(); i.hasNext();) {
551 if (item.equals((String)i.next())) {
552 i.remove();
553 break;
554 }
555 }
556 killring.addFirst(item);
557 }
558
559 /***
560 * Returns killring successor of <code>item</code> and adds
561 * <code>item</code> to killring.
562 *
563 * @param predecessor
564 * @return Returns first item if <code>item == null</code>.
565 */
566 public static String getNext(String predecessor)
567 {
568 if (killring.size() == 0) {
569 return null;
570 }
571
572 if (predecessor == null) {
573 return (String)killring.getFirst();
574 }
575
576 for (Iterator i = killring.iterator(); i.hasNext();) {
577 if (predecessor.equals((String)i.next())) {
578 i.remove();
579 if (i.hasNext()) {
580 String result = (String)i.next();
581 killring.addFirst(predecessor);
582 return result;
583 }
584 else {
585 break;
586 }
587 }
588 }
589
590 if (killring.size() == 0) {
591 return null;
592 }
593 String result = (String)killring.getFirst();
594 killring.addFirst(predecessor);
595 return result;
596 }
597 }
598
599 /***
600 * Pastes an element from the killring cycling through it.
601 */
602 public static class YankPopAction extends TextAction
603 {
604
605 public YankPopAction(String nm)
606 {
607 super(nm);
608 }
609
610 public void actionPerformed(ActionEvent event)
611 {
612 JTextComponent jtc = getTextComponent(event);
613
614 if (jtc != null && YankAction.killring.size() > 0) {
615 jtc.setSelectionStart(YankAction.start);
616 jtc.setSelectionEnd(YankAction.end);
617 String toYank = YankAction.getNext(jtc.getSelectedText());
618 if (toYank != null) {
619 jtc.replaceSelection(toYank);
620 YankAction.end = jtc.getCaretPosition();
621 }
622 else {
623 jtc.getToolkit().beep();
624 }
625 }
626 }
627 }
628
629 /***
630 * Capitalizes the next word.
631 */
632 public static class CapitalizeWordAction extends TextAction
633 {
634 public CapitalizeWordAction(String nm)
635 {
636 super(nm);
637 }
638
639 /***
640 * At first the same code as in {@link
641 * EmacsKeyBindings.DowncaseWordAction} is performed, to ensure the
642 * word is in lower case, then the first letter is capialized. */
643 public void actionPerformed(ActionEvent event)
644 {
645 JTextComponent jtc = getTextComponent(event);
646
647 if (jtc != null) {
648 try {
649
650 int start = jtc.getCaretPosition();
651 int end = Utilities.getNextWord(jtc, start);
652 jtc.setSelectionStart(start);
653 jtc.setSelectionEnd(end);
654 String word = jtc.getText(start, end - start);
655 jtc.replaceSelection(word.toLowerCase());
656
657
658 int offs = Utilities.getWordStart(jtc, start);
659
660 String c = jtc.getText(offs, 1);
661
662 if (c.equals(" ")) {
663
664
665 offs = Utilities.getWordStart(jtc, ++offs);
666 c = jtc.getText(offs, 1);
667 }
668 if (Character.isLetter(c.charAt(0))) {
669 jtc.setSelectionStart(offs);
670 jtc.setSelectionEnd(offs + 1);
671 jtc.replaceSelection(c.toUpperCase());
672 }
673 end = Utilities.getWordEnd(jtc, offs);
674 jtc.setCaretPosition(end);
675 }
676 catch (BadLocationException ble) {
677 jtc.getToolkit().beep();
678 }
679 }
680 }
681 }
682
683 /***
684 * Renders all characters of the next word to lowercase.
685 */
686 public static class DowncaseWordAction extends TextAction
687 {
688 public DowncaseWordAction(String nm)
689 {
690 super(nm);
691 }
692
693 public void actionPerformed(ActionEvent event)
694 {
695 JTextComponent jtc = getTextComponent(event);
696
697 if (jtc != null) {
698 try {
699 int start = jtc.getCaretPosition();
700 int end = Utilities.getNextWord(jtc, start);
701 jtc.setSelectionStart(start);
702 jtc.setSelectionEnd(end);
703 String word = jtc.getText(start, end - start);
704 jtc.replaceSelection(word.toLowerCase());
705 }
706 catch (BadLocationException ble) {
707 jtc.getToolkit().beep();
708 }
709 }
710 }
711 }
712
713 /***
714 * Renders all characters of the next word to upppercase.
715 */
716 public static class UpcaseWordAction extends TextAction
717 {
718 public UpcaseWordAction(String nm)
719 {
720 super(nm);
721 }
722
723 public void actionPerformed(ActionEvent event)
724 {
725 JTextComponent jtc = getTextComponent(event);
726
727 if (jtc != null) {
728 try {
729 int start = jtc.getCaretPosition();
730 int end = Utilities.getNextWord(jtc, start);
731 jtc.setSelectionStart(start);
732 jtc.setSelectionEnd(end);
733 String word = jtc.getText(start, end - start);
734 jtc.replaceSelection(word.toUpperCase());
735 }
736 catch (BadLocationException ble) {
737 jtc.getToolkit().beep();
738 }
739 }
740 }
741 }
742 }