/**
* 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.HashMap;
import java.util.Map;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.keys.KeyBinding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.bindings.keys.ParseException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchListener;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.internal.keys.BindingService;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusActivator;
import com.mulgasoft.emacsplus.EmacsPlusUtils;
import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds;
import com.mulgasoft.emacsplus.execute.IBindingResult;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport;
import com.mulgasoft.emacsplus.minibuffer.BindingMinibuffer;
import com.mulgasoft.emacsplus.minibuffer.IMinibufferState;
import com.mulgasoft.emacsplus.minibuffer.KbdMacroMinibuffer;
import com.mulgasoft.emacsplus.minibuffer.YesNoMinibuffer;
/**
* Implement kbd-macro-bind-to-key
*
* Bind the most recently defined keyboard macro to a key sequence (for the duration of the session).
*
* [Emacs+ addition: if called with ^U then prompt for named kbd macro to bind]
*
* If you try to bind to a key sequence with an existing binding (in any keymap), this
* command asks you for confirmation before replacing the existing binding.
*
* To avoid problems caused by overriding existing bindings, the key
* sequences `C-x C-k 0' through `C-x C-k 9' and `C-x C-k A' through `C-x
* C-k Z' are reserved for your own keyboard macro bindings. In fact, to
* bind to one of these key sequences, you only need to type the digit or
* letter rather than the whole key sequences. For example,
*
* C-x C-k b 4
*
* will bind the last keyboard macro to the key sequence `C-x C-k 4'.
*
* @see org.eclipse.ui.internal.keys.BindingService
* @see org.eclipse.jface.bindings.BindingManager#addBinding(Binding)
*
* @author Mark Feber - initial API and implementation
*/
// for cast to internal org.eclipse.ui.internal.keys.BindingService
@SuppressWarnings("restriction")
public class KbdMacroBindHandler extends KbdMacroDefineHandler {
private static final String BINDING_PREFIX = EmacsPlusActivator.getResourceString("KbdMacro_Bind_Prefix"); //$NON-NLS-1$
private static final String NAMING_PREFIX = EmacsPlusActivator.getResourceString("KbdMacro_BindName_Prefix"); //$NON-NLS-1$
private static final String REBINDING = EmacsPlusActivator.getResourceString("KbdMacro_ReBinding_Prefix"); //$NON-NLS-1$
private static final String DEMAND_ANSWER= EmacsPlusActivator.getResourceString("KbdMacro_ReReBinding_Prefix"); //$NON-NLS-1$
private static final String KBD_BINDING_ERROR= EmacsPlusActivator.getResourceString("KbdMacro_BadBinding"); //$NON-NLS-1$
private static final String BOUND = EmacsPlusActivator.getResourceString("KbdMacro_Bind_Result"); //$NON-NLS-1$
private static final String ABORT = EmacsPlusActivator.getResourceString("KbdMacro_Bind_Abort"); //$NON-NLS-1$
private static final String STD_KBD_PREFIX = "CTRL+X CTRL+K"; //$NON-NLS-1$
// Name constituent TODO: do we need to improve uniqueness?
private static int nameid = 0;
// Name constituent. Not really a lambda, but it's tradition
private final static String KBD_LNAME = "lambda_"; //$NON-NLS-1$
// store bindings so they can be removed on exit
private static Map<String,Binding>cacheBindings = new HashMap<String,Binding>();
// the listener for cleanup on workbench exit
private static IWorkbenchListener workbenchListener = null;
public KbdMacroBindHandler() {
super();
IWorkbench bench = PlatformUI.getWorkbench();
bench.addWorkbenchListener(getWorkbenchListener());
}
private IWorkbenchListener getWorkbenchListener() {
if (workbenchListener == null) {
workbenchListener = new IWorkbenchListener() {
public boolean preShutdown(IWorkbench workbench, boolean forced) {
// comment on IWorkbenchListener says this is too early
return true;
}
public void postShutdown(IWorkbench workbench) {
// hopefully, it is not too late
if (!cacheBindings.isEmpty()) {
IBindingService service = (IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class);
if (service instanceof BindingService) {
BindingService bindingMgr = (BindingService) service;
for (Binding b : cacheBindings.values()) {
bindingMgr.removeBinding(b);
}
}
}
}
};
}
return workbenchListener;
}
/**
* @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#transform(ITextEditor, IDocument, ITextSelection, ExecutionEvent)
*/
@Override
protected int transform(ITextEditor editor, IDocument document, ITextSelection currentSelection,
ExecutionEvent event) throws BadLocationException {
boolean named = isUniversalPresent();
if (KbdMacroSupport.getInstance().hasKbdMacro(named) && !KbdMacroSupport.getInstance().isBusy()) {
mbState = (named ? nameState() : bindState(null));
return mbState.run(editor);
} else {
asyncShowMessage(editor, NO_MACRO_ERROR, true);
}
return NO_OFFSET;
}
/**
* @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#isLooping()
*/
@Override
protected boolean isLooping() {
return false;
}
/**
* Get state to handle prompt for key binding
*
* @return binding prompt state
*/
private IMinibufferState bindState(final String name) {
return new IMinibufferState() {
public String getMinibufferPrefix() {
return BINDING_PREFIX;
}
public int run(ITextEditor editor) {
miniTransform(new BindingMinibuffer(KbdMacroBindHandler.this), editor, null);
return NO_OFFSET;
}
public boolean executeResult(ITextEditor editor, Object minibufferResult) {
boolean result = true;
if (minibufferResult != null) {
IBindingResult bindingR = (IBindingResult) minibufferResult;
String keyString = bindingR.getKeyString();
KeySequence trigger = null;
KeyStroke[] keys = null;
if (bindingR == null || (trigger = bindingR.getTrigger()) == null) {
// no binding present
asyncShowMessage(editor, String.format(CMD_NO_RESULT, keyString) + CMD_NO_BINDING, true);
} else if ((keys = trigger.getKeyStrokes()).length > 0 && keys[keys.length -1].getModifierKeys() != 0) {
// binding ends with a modifier key
if (bindingR.getKeyBinding() != null &&
(IEmacsPlusCommandDefinitionIds.KEYBOARD_QUIT.equals(bindingR.getKeyBinding().getParameterizedCommand().getId()))) {
// clear status area and process ^G interrupt
EmacsPlusUtils.clearMessage(editor);
beep();
} else {
// report bad binding present
asyncShowMessage(editor, KBD_BINDING_ERROR, true);
}
} else if (bindingR.getKeyBinding() != null) {
// conflicting binding present
transitionState(editor,bindingR);
} else if (bindingR.getKeyString().length() == 1){
// single character was entered, check for C-x C-k <key> binding conflict
IBindingResult ibr = getBinding(editor,bindingR.getKeyString().charAt(0));
if (ibr != null) {
if (ibr.getKeyBinding() != null) {
transitionState(editor,ibr);
} else {
addBinding(editor,ibr,name); // good to go
}
} else {
asyncShowMessage(editor, String.format(ABORT, bindingR.getKeyString()), true);
}
} else {
addBinding(editor, bindingR,name); // good to go
}
}
return result;
}
private void transitionState(ITextEditor editor, IBindingResult ibr) {
// conflicting binding present
mbState = yesnoState(ibr,name);
mbState.run(editor);
}
};
}
/**
* Get state to handle prompt for getting kbd macro name
*
* @return naming prompt state
*/
private IMinibufferState nameState() {
return new IMinibufferState() {
public String getMinibufferPrefix() {
return NAMING_PREFIX;
}
public int run(ITextEditor editor) {
miniTransform(new KbdMacroMinibuffer(KbdMacroBindHandler.this),editor,null);
return NO_OFFSET;
}
public boolean executeResult(ITextEditor editor, Object minibufferResult) {
boolean result = true;
if (minibufferResult != null) {
String name = (String) minibufferResult;
if (name == null || name.length() == 0) {
// no name entered
asyncShowMessage(editor, String.format(CMD_NO_RESULT, name) + CMD_NO_BINDING, true);
} else if (KbdMacroSupport.getInstance().getKbdMacro(name) != null) {
// macro by that name
transitionState(editor,name);
} else {
// no macro found
asyncShowMessage(editor, String.format(NO_NAME_UNO, name), true);
}
}
return result;
}
private void transitionState(ITextEditor editor, String name) {
mbState= bindState(name);
mbState.run(editor);
}
};
}
/**
* Get state to handle yes/no prompt
*
* @param binding
* @param mini
* @return yes/no state
*/
private IMinibufferState yesnoState(final IBindingResult binding, final String name) {
return new IMinibufferState() {
IBindingResult bindingResult = binding;
YesNoMinibuffer minibuffer = null;
boolean retry = false;
public String getMinibufferPrefix() {
String command = EMPTY_STR;
try {
command = binding.getKeyBinding().getParameterizedCommand().getName();
} catch (NotDefinedException e) {
}
if (retry) {
return String.format(DEMAND_ANSWER,YESORNO_YES,YESORNO_NO);
} else {
return String.format(REBINDING,binding.getKeyString(),command,YESORNO_YES,YESORNO_NO);
}
}
public int run(ITextEditor editor) {
minibuffer = new YesNoMinibuffer(KbdMacroBindHandler.this);
miniTransform(minibuffer,editor,null);
return NO_OFFSET;
}
public boolean executeResult(ITextEditor editor, Object minibufferResult) {
boolean result = true;
if (minibufferResult != null && minibufferResult instanceof Boolean) {
if ((Boolean)minibufferResult) {
addBinding(editor, bindingResult,name);
} else {
asyncShowMessage(editor, String.format(ABORT, bindingResult.getKeyString()), true);
}
} else {
retry = true;
miniTransform(minibuffer,editor,null);
result = false;
}
return result;
}
};
}
/**
* Check for C-x C-k <key> binding conflict
*
* @param editor
* @param c
* @return IBindingResult with C-x C-k <key> information
*/
private IBindingResult getBinding(ITextEditor editor, char c) {
IBindingResult result = null;
try {
final KeySequence sequence = KeySequence.getInstance(KeySequence.getInstance(STD_KBD_PREFIX), KeyStroke.getInstance(c));
final Binding binding = checkForBinding(editor, sequence);
result = new IBindingResult() {
public Binding getKeyBinding() { return binding; }
public String getKeyString() { return sequence.format(); }
public KeySequence getTrigger() { return sequence; }
};
} catch (ParseException e) { }
return result;
}
/**
* Add the binding to the Emacs+ scheme
*
* @param editor
* @param bindingResult
*/
private void addBinding(ITextEditor editor, IBindingResult bindingResult, String name) {
IBindingService service = (IBindingService) editor.getSite().getService(IBindingService.class);
if (service instanceof BindingService) {
try {
BindingService bindingMgr = (BindingService) service;
if (bindingResult.getKeyBinding() != null) {
// we're overwriting a binding, out with the old
bindingMgr.removeBinding(bindingResult.getKeyBinding());
}
Command command = null;
if (name != null) {
ICommandService ics = (ICommandService) editor.getSite().getService(ICommandService.class);
String id = EmacsPlusUtils.kbdMacroId(name);
// check first, as getCommand will create it if it doesn't already exist
if (ics.getDefinedCommandIds().contains(id)) {
command = ics.getCommand(id);
}
} else {
// use the unexposed category
command = nameKbdMacro(KBD_LNAME + nameid++, editor, KBD_GAZONK);
}
if (command != null) {
Binding binding = new KeyBinding(bindingResult.getTrigger(), new ParameterizedCommand(command, null),
KBD_SCHEMEID, KBD_CONTEXTID, null, null, null, Binding.USER);
bindingMgr.addBinding(binding);
asyncShowMessage(editor, String.format(BOUND, bindingResult.getKeyString()), false);
} else {
asyncShowMessage(editor, String.format(NO_NAME_UNO, name), true);
}
} catch (Exception e) {
asyncShowMessage(editor, String.format(ABORT, bindingResult.getKeyString()), true);
}
}
}
}