/**
* Copyright (c) 2009, 2010 Mark Feber, MulgaSoft
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.mulgasoft.emacsplus.commands;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.Beeper;
import com.mulgasoft.emacsplus.EmacsPlusActivator;
import com.mulgasoft.emacsplus.EmacsPlusUtils;
import com.mulgasoft.emacsplus.IBeepListener;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport.IKbdExecutionListener;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport.KbdEvent;
import org.eclipse.swt.widgets.Display;
/**
* Execute the keyboard macro
* A prefix argument serves as a repeat count
* A prefix argument of zero means repeat until error.
*
* @author Mark Feber - initial API and implementation
*/
public class KbdMacroExecuteHandler extends EmacsPlusNoEditHandler {
// we're not truly no-edit, but let any sub-commands decide (Eclipse will ignore buffer text input for us)
protected static final String KBD_INTERRUPTED = EmacsPlusActivator.getResourceString("KbdMacro_Interrupted"); //$NON-NLS-1$
protected static final String KBD_ITERATION = EmacsPlusActivator.getResourceString("KbdMacro_Iteration"); //$NON-NLS-1$
protected static final String KBD_ITERATIONS = EmacsPlusActivator.getResourceString("KbdMacro_Iterations"); //$NON-NLS-1$
protected static final String KBD_BINDING_WARNING = "KbdMacro_BadBinding"; //$NON-NLS-1$
protected static final String NO_MACRO_ERROR = "KbdMacro_No_Error"; //$NON-NLS-1$
// Thread name when executing
private static final String KBD_THREAD = "Kbd Macro Execution"; //$NON-NLS-1$
private static final int KEY_WAIT = 5000;
private static final int LONG_WAIT = 10000;
private static boolean interrupted = false;
private static int executeCount = 0; // keep track of iterative and nested execution
private String kbdMacroName = null;
void setKbdMacroName(String kbdMacroName) {
this.kbdMacroName = kbdMacroName;
}
String getKbdMacroName() {
return kbdMacroName;
}
protected int incrementExecutionCount() {
return executeCount++;
}
private int decrementExecutionCount() {
return executeCount--;
}
protected class MacroCount {
private int counter = 0;
void addCounter() {
++counter;
}
int getCounter() {
return counter;
}
}
@Override
protected Object transformWithCount(ITextEditor editor, IDocument document, ITextSelection selection, ExecutionEvent event) {
Event e = null;
if (event != null && (e = (Event)event.getTrigger()) != null) {
// disallow macro execution with modifier key
if (e.stateMask != 0) {
beep();
asyncShowMessage(editor, KBD_BINDING_WARNING, true);
return null;
}
}
MacroCount keepCount = null;
if (hasKbdMacro()) {
int count = Math.abs(getUniversalCount());
if (incrementExecutionCount() == 0) {
// only add first time (not on iterative/nested invocations)
addBeeper();
keepCount = new MacroCount();
if (count == 0) {
count = Integer.MAX_VALUE; // essentially forever
}
}
// synchronize key listeners for this invocation (may be nested)
KbdLock kbdLock = new KbdLock();
pushExecution(editor,kbdLock);
runMacro(editor, document, selection, kbdLock, count, event.getCommand().getId(),keepCount);
} else {
beep();
asyncShowMessage(editor, NO_MACRO_ERROR, true);
}
return null;
}
protected boolean hasKbdMacro() {
return KbdMacroSupport.getInstance().hasKbdMacro() && !KbdMacroSupport.getInstance().isBusy();
}
protected void runMacro(ITextEditor editor, IDocument document, ITextSelection selection, KbdLock vkf, int count,
final String cmdId) {
runMacro(editor, document, selection, vkf, count, cmdId, null);
}
/**
* Run each instance of the macro in a non-ui thread, so it can wait for each keyboard (and other)
* event to be processed as both key and asynchronous events are processed in the same loop in
* org.eclipse.swt.widgets.Display.readAndDispatch() which unfortunately can run all asynchronous
* requests before processing the next key event depending on the timing of their arrival
*
* @param editor
* @param document
* @param selection
* @param vkf
* @param count
* @param cmdId
* @param keepCount
*/
protected void runMacro(final ITextEditor editor,final IDocument document,final ITextSelection selection, final KbdLock vkf, final int count,
final String cmdId, final MacroCount keepCount) {
new Thread(new Runnable() {public void run() {
int counter = count;
// get the undo Runnable wrappers
Runnable[] undo = undoProtect(editor, keepCount);
try {
EmacsPlusUtils.asyncUiRun(undo[0]);
if (!isInterrupted() && selection != null && checkSelection(selection)) {
executeOnce(editor, document, selection, vkf);
}
} catch (Exception e) {
beep();
} finally {
EmacsPlusUtils.asyncUiRun(undo[1]);
}
if (!isInterrupted() && --counter > 0) {
runMacro(editor, document, selection, vkf, counter, cmdId, keepCount);
} else {
// notify listener on exit if we were executing a named or bound kbd macro
notifyKbdListener(cmdId);
final int times = ((keepCount != null) ? keepCount.getCounter() : 0);
EmacsPlusUtils.asyncUiRun(new Runnable() {
public void run() {
decrementExecutionCount();
if (popExecution(editor) == 0 && isInterrupted()) {
EmacsPlusUtils.asyncUiRun(new Runnable() {public void run() {
asyncShowMessage(editor, KBD_INTERRUPTED + String.format(((times == 1) ? KBD_ITERATION : KBD_ITERATIONS),times), true);}});
}
}
});
}
}},KBD_THREAD).start();
}
/**
* Iterate through the macro events once.
* After each event is submitted to the ui-thread wait for the event to be processed before moving to the next event
* This guarantees that keyboard events will be read properly before the next event is submitted
*
* @param editor
* @param document
* @param currentSelection
* @param vkf
* @throws BadLocationException
*/
protected void executeOnce(ITextEditor editor, IDocument document, ITextSelection currentSelection, KbdLock vkf)
throws BadLocationException {
ArrayList<KbdEvent> macro = (kbdMacroName == null) ? KbdMacroSupport.getInstance().getKbdMacroEvents()
: KbdMacroSupport.getInstance().getKbdMacroEvents(kbdMacroName);
if (editor != null && macro != null && !macro.isEmpty()) {
for (KbdEvent e : macro) {
if (!isInterrupted()) {
queueRunner(e,editor,vkf);
}
}
}
}
/**
* Create and queue the appropriate Runnable for the kbd event type
*
* @param event the KbdEvent
* @param editor
* @return the new Runnable
*/
private Runnable queueRunner(KbdEvent event, ITextEditor editor, KbdLock vkf) {
Runnable result = null;
String cmdId = null;
if (event.getEvent() != null) { // It's a key event
result = getKeyRunner(event, editor);
CountDownLatch latch = new CountDownLatch(1);
vkf.setLatch(latch);
EmacsPlusUtils.asyncUiRun(result);
try {
if (!isInterrupted()){
//Event e = event.getEvent();
// System.out.println("wait " + event.toString() + " " + e.character + " " + e.keyCode
// + " " + e.stateMask + " " + e.type + " " + e.doit);
// if we see a beep interrupt, stop waiting
myBeeper.setKeyLock(vkf);
if (event.isWait()) {
// wait for simulated key event to be processed before going to the next kbd event
latch.await(KEY_WAIT,TimeUnit.MILLISECONDS);
}
}
} catch (InterruptedException e) {
} finally {
vkf.setLatch(null); // we're the only waiter
myBeeper.setKeyLock(null);
}
} else if ((cmdId = event.getCmd()) != null) { // It's a command id
result = getCmdRunner(event, editor, vkf);
// register for notification of macro completion
addKbdListener(cmdId);
EmacsPlusUtils.asyncUiRun(result);
// wait until the macro completes
waitForKbdListener(cmdId);
} else if (event.isExit()) { // It's an exit from a minibuffer
result = getExitRunner(vkf);
EmacsPlusUtils.asyncUiRun(result);
}
return result;
}
/**
* Create the Runnable for executing a command (which may be a named/bound kbd macro)
*
* @param event
* @param editor
* @param vkf
* @return the Runnable
*/
private Runnable getCmdRunner(KbdEvent event, final ITextEditor editor, final KbdLock vkf) {
final String cmdId = event.getCmd();
@SuppressWarnings("unchecked")
final Map<String,?> parameters = (Map<String,?>) event.getCmdParameters();
final KbdMacroExecuteHandler executeHandler = this;
return new Runnable() {
public void run() {
if (executeHandler.isInterrupted()) {
return;
}
try {
ITextEditor current = EmacsPlusUtils.getCurrentEditor();
if (parameters != null) {
KbdMacroExecuteHandler.this.executeCommand(cmdId, parameters, null,
(current != null ? current : editor));
} else {
KbdMacroExecuteHandler.this.executeCommand(cmdId, null,
(current != null ? current : editor));
}
} catch (Exception e) {
if (isMacro(cmdId)) {
notifyKbdListener(cmdId);
}
}
}
};
}
/**
* Create the Runnable for posting a key event
*
* @param event
* @param editor
* @return the Runnable
*/
private Runnable getKeyRunner(final KbdEvent event, ITextEditor editor) {
final KbdMacroExecuteHandler executeHandler = this;
return new Runnable() {
Event ee = event.getEvent();
public void run() {
if (executeHandler.isInterrupted()){
return;
}
// to support internal Emacs+ control sequences in minibuffer commands:
// The .post event ignores the stateMask, so force CTRL/ALT/Shift keys manually when required
Event shiftless = null;
Event ctrlless = null;
Event altless = null;
Event cmdless = null;
Display display = PlatformUI.getWorkbench().getDisplay();
try {
if (ee.stateMask != 0) {
if ((ee.stateMask & SWT.SHIFT) != 0){
shiftless = KbdMacroExecuteHandler.this.maskEvent(SWT.SHIFT);
display.post(shiftless);
}
if ((ee.stateMask & SWT.CTRL) != 0) {
ctrlless = KbdMacroExecuteHandler.this.maskEvent(SWT.CTRL);
display.post(ctrlless);
}
if ((ee.stateMask & SWT.ALT) != 0) {
altless = KbdMacroExecuteHandler.this.maskEvent(SWT.ALT);
display.post(altless);
}
if ((ee.stateMask & SWT.COMMAND) != 0) {
cmdless = KbdMacroExecuteHandler.this.maskEvent(SWT.COMMAND);
display.post(cmdless);
}
}
ee.doit = true;
ee.type = SWT.KeyDown;
executeHandler.setKeyEvent(ee);
display.post(ee);
// System.out.println("post " + ee.character);
} finally {
if (ee.stateMask != 0) {
if (shiftless != null) {
shiftless.type = SWT.KeyUp;
shiftless.doit = true;
display.post(shiftless);
}
if (ctrlless != null) {
ctrlless.type = SWT.KeyUp;
ctrlless.doit = true;
display.post(ctrlless);
}
if (altless != null) {
altless.type = SWT.KeyUp;
altless.doit = true;
display.post(altless);
}
if (cmdless != null) {
cmdless.type = SWT.KeyUp;
cmdless.doit = true;
display.post(cmdless);
}
}
}
}
};
}
/**
* Create a Runnable to ensure the proper exit from a minibuffer command
* @param vkf
* @return the Runnable
*/
private Runnable getExitRunner(final KbdLock vkf) {
return new Runnable() {
public void run() {
if (KbdMacroSupport.getKbdMinibuffer() != null) {
KbdMacroSupport.getKbdMinibuffer().endSession();
}
}
};
}
/**
* Create a simple event with a stateMask based keyCode
*
* @param mask
* @return
*/
private Event maskEvent(int mask) {
Event x = new Event();
x.keyCode = mask;
x.type = SWT.KeyDown;
x.doit = true;
return x;
}
/**
* Get the undo Runnable wrappers
*
* @param editor
* @return begin and end undoProtect wrappers
*/
protected Runnable[] undoProtect(ITextEditor editor, final MacroCount keepCount) {
Runnable[] result = new Runnable[2];
// use widget to avoid unpleasant scrolling side effects of IRewriteTarget
final Control widget = getTextWidget(editor);;
final IRewriteTarget rt = (IRewriteTarget) editor.getAdapter(IRewriteTarget.class);;
result[0] = new Runnable() {
public void run() {
if (rt != null) {
rt.beginCompoundChange();
}
setRedraw(widget,false);
}
};
result[1] = new Runnable() {
public void run() {
setRedraw(widget, true);
if (rt != null) {
rt.endCompoundChange();
}
if (!isInterrupted() && keepCount != null) {
// we've finished one more loop of the main macro
keepCount.addCounter();
}
}
};
return result;
}
/**
* Determine if the id belongs to a kbd macro execution command
*
* @param id - command id
* @return true if will execute a kbd macro
*/
private boolean isMacro(String id) {
// verify id type
return EmacsPlusUtils.isMacroId(id);
}
/* Listener support for detecting inner kbd macro completion
* If the command invocation is a named kbd macro, then wait for it's nested invocation
* to complete before proceeding with the calling kbd macro
*/
private static Map<String,CountDownLatch>kbdListeners = new Hashtable<String,CountDownLatch>();
private void notifyKbdListener(String key) {
CountDownLatch lock = kbdListeners.get(key);
if (lock != null) {
kbdListeners.remove(key);
lock.countDown();
}
}
private void waitForKbdListener(String key) {
CountDownLatch lock = null;
if (isMacro(key) && (lock = kbdListeners.get(key)) != null) {
try {
lock.await(LONG_WAIT,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {}
}
}
private void addKbdListener(String key) {
if (isMacro(key)) {
kbdListeners.put(key, new CountDownLatch(1));
}
}
/* Listener support for detecting beep errors */
private static KbdBeepListener myBeeper = new KbdBeepListener();
private static class KbdBeepListener implements IBeepListener {
private KbdLock keywait = null;
public void beepInterrupt() {
setInterrupted(true);
Beeper.removeBeepListener(this);
if (keywait != null) {
keywait.notifyLock();
}
// exit minibuffer - it's often an ISearch
if (KbdMacroSupport.getKbdMinibuffer() != null) {
KbdMacroSupport.getKbdMinibuffer().endSession();
}
}
public void setKeyLock(KbdLock keywait) {
this.keywait = keywait;
}
};
void addBeeper() {
setInterrupted(false);
Beeper.addBeepListener(myBeeper);
}
void removeBeeper() {
Beeper.removeBeepListener(myBeeper);
}
public final boolean isInterrupted() {
return interrupted;
}
private static void setInterrupted(boolean interrupt) {
KbdMacroExecuteHandler.interrupted = interrupt;
}
/* Support for maintaining correct key listeners during execution */
// remember the key listeners of preceding kbd macros on the stack
private static Stack<KbdLock> lockStack = new Stack<KbdLock>();
protected void pushExecution(ITextEditor editor, KbdLock newLock) {
if (!lockStack.isEmpty()) {
KbdMacroSupport.getInstance().setExecuting(false, editor,lockStack.peek());
}
lockStack.push(newLock);
KbdMacroSupport.getInstance().setExecuting(true, editor,newLock);
}
/**
* Remove current key listener and restore previous if present
*
* @param editor
* @return the number or locks remaining in the stack
*/
protected int popExecution(ITextEditor editor) {
if (!lockStack.isEmpty()) {
KbdMacroSupport.getInstance().setExecuting(false, editor,lockStack.pop());
if (!lockStack.isEmpty()) {
KbdMacroSupport.getInstance().setExecuting(true, editor,lockStack.peek());
} else {
// we're done with all execution
notifyExecutionListeners();
removeBeeper();
}
}
return lockStack.size();
}
/* Listener support for notification after execution */
private static List<IKbdExecutionListener> executionListners = new Vector<IKbdExecutionListener>();
public static synchronized void addExecutionListener(IKbdExecutionListener listener) {
executionListners.add(listener);
}
public synchronized void notifyExecutionListeners() {
for (IKbdExecutionListener k : executionListners) {
synchronized(k) {
k.executionDone();
}
}
executionListners.clear();
}
/* Key Listener support to detect any real keyboard events */
// since our key events are processed serially, we can detect if user does something to interrupt
static private int keyCode = 0;
static private int stateMask = 0;
private void setKeyEvent(Event e){
keyCode = e.keyCode;
stateMask = e.stateMask;
}
private void clearKeyEvent(){
keyCode = 0;
stateMask = 0;
}
/**
* Check if this is one of the manually CTRL/ALT/Command/Shift keys from the .post call above
* keyCode is a simple state key and is included in contained event's stateMask
*
* @param keyCode from the verify key event
* @return true if it is ours
*/
private boolean isSpecial(int keyCode) {
return (((keyCode == SWT.ALT ) || (keyCode == SWT.COMMAND ) || (keyCode == SWT.CTRL) || (keyCode == SWT.SHIFT)) && ((keyCode & stateMask) != 0));
}
public class KbdLock implements VerifyKeyListener {
private CountDownLatch latch = null;
private CountDownLatch setLatch(CountDownLatch latch) {
CountDownLatch result = this.latch;
this.latch = latch;
return result;
}
public void notifyLock() {
try {
if (latch != null) {
latch.countDown();
}
} catch (Exception e) {}
}
/**
* @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent)
*/
public void verifyKey(VerifyEvent event) {
// ignore the stateMask keyCodes, otherwise require a match
if (!isSpecial(event.keyCode)) {
// A real (user) key event must have happened
if (event.keyCode != keyCode || event.stateMask != stateMask) {
event.doit = false;
// short circuit and beep
setInterrupted(true);
Beeper.beep(); // this will notify
} else {
// detect kbd macro key event
clearKeyEvent();
notifyLock();
PlatformUI.getWorkbench().getDisplay().post(upEvent(event));
}
}
}
// Linux, at least, requires this up event, but on other systems can't be posted until
// we've received the KeyDown event
private Event upEvent(VerifyEvent event) {
Event e = new Event();
e.keyCode = event.keyCode;
e.character = event.character;
e.stateMask = event.stateMask;
e.type = SWT.KeyUp;
e.doit = true;
return e;
}
}
}