001: /*
002: * InputHandler.java - Manages key bindings and executes actions
003: * :tabSize=8:indentSize=8:noTabs=false:
004: * :folding=explicit:collapseFolds=1:
005: *
006: * Copyright (C) 1999, 2003 Slava Pestov
007: *
008: * This program is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU General Public License
010: * as published by the Free Software Foundation; either version 2
011: * of the License, or any later version.
012: *
013: * This program is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with this program; if not, write to the Free Software
020: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
021: */
022:
023: package org.gjt.sp.jedit.gui;
024:
025: //{{{ Imports
026: import javax.swing.*;
027: import javax.swing.text.JTextComponent;
028:
029: import org.gjt.sp.jedit.textarea.JEditTextArea;
030: import org.gjt.sp.jedit.*;
031: import org.gjt.sp.jedit.buffer.JEditBuffer;
032: import org.gjt.sp.jedit.input.AbstractInputHandler;
033: import org.gjt.sp.util.Log;
034:
035: import java.awt.event.KeyEvent;
036: import java.awt.*;
037:
038: //}}}
039:
040: /**
041: * An input handler converts the user's key strokes into concrete actions.
042: * It also takes care of macro recording and action repetition.<p>
043: *
044: * This class provides all the necessary support code for an input
045: * handler, but doesn't actually do any key binding logic. It is up
046: * to the implementations of this class to do so.
047: *
048: * @author Slava Pestov
049: * @version $Id: InputHandler.java 10775 2007-09-30 20:52:43Z kpouer $
050: * @see org.gjt.sp.jedit.gui.DefaultInputHandler
051: */
052: public abstract class InputHandler extends AbstractInputHandler {
053: //{{{ InputHandler constructor
054: /**
055: * Creates a new input handler.
056: * @param view The view
057: */
058: public InputHandler(View view) {
059: super ();
060: this .view = view;
061: } //}}}
062:
063: //{{{ addKeyBinding() method
064: /**
065: * Adds a key binding to this input handler.
066: * @param keyBinding The key binding (the format of this is
067: * input-handler specific)
068: * @param action The action
069: */
070: public abstract void addKeyBinding(String keyBinding, String action);
071:
072: //}}}
073:
074: //{{{ addKeyBinding() method
075: /**
076: * Adds a key binding to this input handler.
077: * @param keyBinding The key binding (the format of this is
078: * input-handler specific)
079: * @param action The action
080: */
081: public abstract void addKeyBinding(String keyBinding,
082: EditAction action);
083:
084: //}}}
085:
086: //{{{ removeKeyBinding() method
087: /**
088: * Removes a key binding from this input handler.
089: * @param keyBinding The key binding
090: */
091: public abstract void removeKeyBinding(String keyBinding);
092:
093: //}}}
094:
095: //{{{ removeAllKeyBindings() method
096: /**
097: * Removes all key bindings from this input handler.
098: */
099: public abstract void removeAllKeyBindings();
100:
101: //}}}
102:
103: //{{{ handleKey() method
104: /**
105: * Handles a keystroke.
106: * @param keyStroke The key stroke.
107: * @return true if the input could be handled.
108: * @since jEdit 4.2pre5
109: */
110: public final boolean handleKey(KeyEventTranslator.Key keyStroke) {
111: return handleKey(keyStroke, false);
112: } //}}}
113:
114: //{{{ processKeyEvent() method
115: /**
116: * Forwards key events directly to the input handler.
117: * This is slightly faster than using a KeyListener
118: * because some Swing overhead is avoided.
119: * @since 4.3pre7
120: */
121: public void processKeyEvent(KeyEvent evt, int from, boolean global) {
122: if (Debug.DUMP_KEY_EVENTS) {
123: Log.log(Log.DEBUG, this , "Key event : "
124: + GrabKeyDialog.toString(evt) + " from " + from);
125: Log.log(Log.DEBUG, this , view + ".isFocused()="
126: + view.isFocused() + '.', new Exception());
127: }
128:
129: if (view.getTextArea().hasFocus() && from == View.VIEW)
130: return;
131:
132: evt = _preprocessKeyEvent(evt);
133: if (evt == null)
134: return;
135:
136: if (Debug.DUMP_KEY_EVENTS) {
137: Log.log(Log.DEBUG, this , "Key event after workaround: "
138: + GrabKeyDialog.toString(evt) + " from " + from);
139: }
140:
141: Component prefixFocusOwner = view.getPrefixFocusOwner();
142: boolean focusOnTextArea = false;
143: switch (evt.getID()) {
144: case KeyEvent.KEY_TYPED:
145: // if the user pressed eg C+e n n in the
146: // search bar we want focus to go back there
147: // after the prefix is done
148: if (prefixFocusOwner != null) {
149: if (prefixFocusOwner.isShowing()) {
150: prefixFocusOwner.requestFocus();
151: focusOnTextArea = true;
152: }
153: }
154:
155: if (keyEventInterceptor != null)
156: keyEventInterceptor.keyTyped(evt);
157: else if (from == View.ACTION_BAR
158: || (Debug.GLOBAL_SHORTCUTS_FOR_DOCKED_DOCKABLES && Options.SIMPLIFIED_KEY_HANDLING)
159: || isPrefixActive()
160: || view.getTextArea().hasFocus()) {
161: processKeyEventKeyStrokeHandling(evt, from, "type ",
162: global);
163: }
164:
165: processKeyEventSub(focusOnTextArea);
166:
167: break;
168: case KeyEvent.KEY_PRESSED:
169: if (keyEventInterceptor != null)
170: keyEventInterceptor.keyPressed(evt);
171: else if (KeyEventWorkaround.isBindable(evt.getKeyCode())) {
172: if (prefixFocusOwner != null) {
173: if (prefixFocusOwner.isShowing()) {
174: prefixFocusOwner.requestFocus();
175: focusOnTextArea = true;
176: }
177: view.setPrefixFocusOwner(null);
178: }
179:
180: processKeyEventKeyStrokeHandling(evt, from, "press",
181: global);
182:
183: processKeyEventSub(focusOnTextArea);
184:
185: }
186: break;
187: case KeyEvent.KEY_RELEASED:
188: if (keyEventInterceptor != null)
189: keyEventInterceptor.keyReleased(evt);
190: break;
191: }
192: } //}}}
193:
194: //{{{ _preprocessKeyEvent() method
195: private KeyEvent _preprocessKeyEvent(KeyEvent evt) {
196: if (view.isClosed())
197: return null;
198: Component focusOwner = view.getFocusOwner();
199: if (Options.SIMPLIFIED_KEY_HANDLING) {
200: /*
201: It seems that the "else" path below does
202: not work. Apparently, is is there to prevent
203: some keyboard events to be "swallowed" by
204: jEdit when the keyboard event in fact should
205: be scheduled to swing for further handling.
206:
207: On some "key typed" events, the "return null;"
208: is triggered. However, these key events
209: actually do not seem to be handled elseewhere,
210: so they are not handled at all.
211:
212: This behaviour exists with old keyboard handling
213: as well as with new keyboard handling. However,
214: the new keyboard handling is more sensitive
215: about what kinds of key events it receives. It
216: expects to see all "key typed" events,
217: which is incompatible with the "return null;"
218: below.
219:
220: This bug triggers jEdit bug 1493185 ( https://sourceforge.net/tracker/?func=detail&aid=1493185&group_id=588&atid=100588 ).
221:
222: Thus, we disable the possibility of
223: key event swallowing for the new key event
224: handling.
225:
226: */
227: } else {
228: if (focusOwner instanceof JComponent) {
229: JComponent comp = (JComponent) focusOwner;
230: InputMap map = comp.getInputMap();
231: ActionMap am = comp.getActionMap();
232:
233: if (map != null && am != null && comp.isEnabled()) {
234: KeyStroke keyStroke = KeyStroke
235: .getKeyStrokeForEvent(evt);
236: Object binding = map.get(keyStroke);
237: if (binding != null && am.get(binding) != null) {
238: return null;
239: }
240: }
241: }
242: }
243:
244: if (focusOwner instanceof JTextComponent) {
245: // fix for the bug where key events in JTextComponents
246: // inside views are also handled by the input handler
247: if (evt.getID() == KeyEvent.KEY_PRESSED) {
248: switch (evt.getKeyCode()) {
249: case KeyEvent.VK_ENTER:
250: case KeyEvent.VK_TAB:
251: case KeyEvent.VK_BACK_SPACE:
252: case KeyEvent.VK_SPACE:
253: return null;
254: }
255: }
256: }
257:
258: if (evt.isConsumed())
259: return null;
260:
261: if (Debug.DUMP_KEY_EVENTS) {
262: Log.log(Log.DEBUG, this , "Key event (preprocessing) : "
263: + GrabKeyDialog.toString(evt));
264: }
265:
266: return KeyEventWorkaround.processKeyEvent(evt);
267: } //}}}
268:
269: //{{{ processKeyEventSub() method
270: private void processKeyEventSub(boolean focusOnTextArea) {
271: // we might have been closed as a result of
272: // the above
273: if (view.isClosed())
274: return;
275:
276: // this is a weird hack.
277: // we don't want C+e a to insert 'a' in the
278: // search bar if the search bar has focus...
279: if (isPrefixActive()) {
280: Component focusOwner = view.getFocusOwner();
281: if (focusOwner instanceof JTextComponent) {
282: view.setPrefixFocusOwner(focusOwner);
283: view.getTextArea().requestFocus();
284: } else if (focusOnTextArea) {
285: view.getTextArea().requestFocus();
286: } else {
287: view.setPrefixFocusOwner(null);
288: }
289: } else {
290: view.setPrefixFocusOwner(null);
291: }
292: }
293:
294: //}}}
295:
296: //{{{ getRepeatCount() method
297: /**
298: * Returns the number of times the next action will be repeated.
299: */
300: public int getRepeatCount() {
301: return repeatCount;
302: } //}}}
303:
304: //{{{ setRepeatCount() method
305: /**
306: * Sets the number of times the next action will be repeated.
307: * @param repeatCount The repeat count
308: */
309: public void setRepeatCount(int repeatCount) {
310: int oldRepeatCount = this .repeatCount;
311: this .repeatCount = repeatCount;
312: if (oldRepeatCount != repeatCount)
313: view.getStatus().setMessage(null);
314: } //}}}
315:
316: //{{{ getLastAction() method
317: /**
318: * Returns the last executed action.
319: * @since jEdit 2.5pre5
320: */
321: public EditAction getLastAction() {
322: return lastAction;
323: } //}}}
324:
325: //{{{ readNextChar() method
326: /**
327: * Invokes the specified BeanShell code, replacing __char__ in the
328: * code with the next input character.
329: * @param msg The prompt to display in the status bar
330: * @param code The code
331: * @since jEdit 3.2pre2
332: */
333: public void readNextChar(String msg, String code) {
334: view.getStatus().setMessage(msg);
335: readNextChar = code;
336: } //}}}
337:
338: //{{{ readNextChar() method
339: /**
340: * @deprecated Use the other form of this method instead
341: */
342: public void readNextChar(String code) {
343: readNextChar = code;
344: } //}}}
345:
346: //{{{ invokeAction() method
347: /**
348: * Invokes the specified action, repeating and recording it as
349: * necessary.
350: * @param action The action
351: * @since jEdit 4.2pre1
352: */
353: public void invokeAction(String action) {
354: invokeAction(jEdit.getAction(action));
355: } //}}}
356:
357: //{{{ invokeAction() method
358: /**
359: * Invokes the specified action, repeating and recording it as
360: * necessary.
361: * @param action The action
362: */
363: public void invokeAction(EditAction action) {
364: JEditBuffer buffer = view.getBuffer();
365:
366: /* if(buffer.insideCompoundEdit())
367: buffer.endCompoundEdit(); */
368:
369: // remember the last executed action
370: if (!action.noRememberLast()) {
371: HistoryModel.getModel("action").addItem(action.getName());
372: if (lastAction == action)
373: lastActionCount++;
374: else {
375: lastAction = action;
376: lastActionCount = 1;
377: }
378: }
379:
380: // remember old values, in case action changes them
381: int _repeatCount = repeatCount;
382:
383: // execute the action
384: if (action.noRepeat() || _repeatCount == 1)
385: action.invoke(view);
386: else {
387: // stop people doing dumb stuff like C+ENTER 100 C+n
388: if (_repeatCount > REPEAT_COUNT_THRESHOLD) {
389: String label = action.getLabel();
390: if (label == null)
391: label = action.getName();
392: else
393: label = GUIUtilities.prettifyMenuLabel(label);
394:
395: Object[] pp = { label, _repeatCount };
396:
397: if (GUIUtilities.confirm(view, "large-repeat-count",
398: pp, JOptionPane.WARNING_MESSAGE,
399: JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
400: repeatCount = 1;
401: view.getStatus().setMessage(null);
402: return;
403: }
404: }
405:
406: try {
407: buffer.beginCompoundEdit();
408:
409: for (int i = 0; i < _repeatCount; i++)
410: action.invoke(view);
411: } finally {
412: buffer.endCompoundEdit();
413: }
414: }
415:
416: Macros.Recorder recorder = view.getMacroRecorder();
417:
418: if (recorder != null && !action.noRecord())
419: recorder.record(_repeatCount, action.getCode());
420:
421: // If repeat was true originally, clear it
422: // Otherwise it might have been set by the action, etc
423: if (_repeatCount != 1) {
424: // first of all, if this action set a
425: // readNextChar, do not clear the repeat
426: if (readNextChar != null)
427: return;
428:
429: repeatCount = 1;
430: view.getStatus().setMessage(null);
431: }
432: } //}}}
433:
434: //{{{ invokeLastAction() method
435: public void invokeLastAction() {
436: if (lastAction == null)
437: view.getToolkit().beep();
438: else
439: invokeAction(lastAction);
440: } //}}}
441:
442: //{{{ Instance variables
443: protected final View view;
444:
445: protected EditAction lastAction;
446:
447: //}}}
448:
449: //{{{ userInput() method
450: protected void userInput(char ch) {
451: lastActionCount = 0;
452:
453: JEditTextArea textArea = view.getTextArea();
454:
455: /* Buffer buffer = view.getBuffer();
456: if(!buffer.insideCompoundEdit())
457: buffer.beginCompoundEdit(); */
458:
459: if (repeatCount == 1)
460: textArea.userInput(ch);
461: else {
462: // stop people doing dumb stuff like C+ENTER 100 C+n
463: if (repeatCount > REPEAT_COUNT_THRESHOLD) {
464: Object[] pp = { String.valueOf(ch), repeatCount };
465:
466: if (GUIUtilities.confirm(view,
467: "large-repeat-count.user-input", pp,
468: JOptionPane.WARNING_MESSAGE,
469: JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
470: repeatCount = 1;
471: view.getStatus().setMessage(null);
472: return;
473: }
474: }
475:
476: JEditBuffer buffer = view.getBuffer();
477: try {
478: if (repeatCount != 1)
479: buffer.beginCompoundEdit();
480: for (int i = 0; i < repeatCount; i++)
481: textArea.userInput(ch);
482: } finally {
483: if (repeatCount != 1)
484: buffer.endCompoundEdit();
485: }
486: }
487:
488: Macros.Recorder recorder = view.getMacroRecorder();
489:
490: if (recorder != null) {
491: recorder.recordInput(repeatCount, ch, textArea
492: .isOverwriteEnabled());
493: }
494:
495: repeatCount = 1;
496: } //}}}
497:
498: //{{{ invokeReadNextChar() method
499: protected void invokeReadNextChar(char ch) {
500: JEditBuffer buffer = view.getBuffer();
501:
502: /* if(buffer.insideCompoundEdit())
503: buffer.endCompoundEdit(); */
504:
505: String charStr = MiscUtilities.charsToEscapes(String
506: .valueOf(ch));
507:
508: // this might be a bit slow if __char__ occurs a lot
509: int index;
510: while ((index = readNextChar.indexOf("__char__")) != -1) {
511: readNextChar = readNextChar.substring(0, index) + '\''
512: + charStr + '\''
513: + readNextChar.substring(index + 8);
514: }
515:
516: Macros.Recorder recorder = view.getMacroRecorder();
517: if (recorder != null)
518: recorder.record(getRepeatCount(), readNextChar);
519:
520: view.getStatus().setMessage(null);
521:
522: if (getRepeatCount() != 1) {
523: try {
524: buffer.beginCompoundEdit();
525:
526: BeanShell.eval(view, BeanShell.getNameSpace(),
527: "for(int i = 1; i < " + getRepeatCount()
528: + "; i++)\n{\n" + readNextChar + "\n}");
529: } finally {
530: buffer.endCompoundEdit();
531: }
532: } else
533: BeanShell
534: .eval(view, BeanShell.getNameSpace(), readNextChar);
535:
536: readNextChar = null;
537: } //}}}
538: }
|