/**
* Copyright (c) 2009, 2014 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.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Collection;
import java.util.SortedMap;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.keys.KeyBinding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
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.execute.KbdMacroSupport;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport.KbdEvent;
import com.mulgasoft.emacsplus.execute.KbdMacroSupport.KbdMacro;
import com.mulgasoft.emacsplus.minibuffer.IMinibufferState;
import com.mulgasoft.emacsplus.minibuffer.KbdMacroMinibuffer;
import static com.mulgasoft.emacsplus.EmacsPlusUtils.getTotalBindings;
/**
* Implement load-kbd-macro
*
* Load a Kbd Macro into the current Eclipse session
*
* @author Mark Feber - initial API and implementation
*/
@SuppressWarnings("restriction") // Need internal class: org.eclipse.ui.internal.keys.BindingService
public class KbdMacroLoadHandler extends KbdMacroFileHandler {
private final static String KBD_LOAD_PREFIX = EmacsPlusActivator.getResourceString("KbdMacro_Load_Prefix"); //$NON-NLS-1$
private final static String MACRO_QUESTION = EmacsPlusActivator.getResourceString("KbdMacro_Name_Exists"); //$NON-NLS-1$
private final static String BINDING_QUESTION = EmacsPlusActivator.getResourceString("KbdMacro_Exists"); //$NON-NLS-1$
private final static String LOADED = EmacsPlusActivator.getResourceString("KbdMacro_Loaded"); //$NON-NLS-1$
private final static String LOADED_AND_BOUND = EmacsPlusActivator.getResourceString("KbdMacro_Bound_Loaded"); //$NON-NLS-1$
private final static String ABORT_LOAD = EmacsPlusActivator.getResourceString("KbdMacro_Abort_Load"); //$NON-NLS-1$
private final static String MACRO_MISSING = EmacsPlusActivator.getResourceString("KbdMacro_Missing"); //$NON-NLS-1$
private final static String BAD_CMD = EmacsPlusActivator.getResourceString("KbdMacro_Bad_Cmd"); //$NON-NLS-1$
private final static String BAD_MACRO_STATE = "KbdMacro_Bad_State"; //$NON-NLS-1$
private final static String FORMAT_S = "%s"; //$NON-NLS-1$
private SortedMap<String,String> fileCompletions = null;
private String resultString = null;
public Object execute(ExecutionEvent event) throws ExecutionException {
try {
if (KbdMacroSupport.getInstance().suppressMessages()) {
try {
ITextEditor editor = null;
IWorkbenchPage page = EmacsPlusUtils.getWorkbenchPage();
if (page != null) {
IEditorPart epart = page.getActiveEditor();
if (epart != null) {
editor = (ITextEditor) epart.getAdapter(ITextEditor.class);
}
}
// short circuit on auto-load invocation
transform(editor,null,null,event);
} catch (BadLocationException e) {
// won't happen: not a location style command
}
} else {
super.execute(event);
}
return resultString;
} finally {
resultString = null;
}
}
protected void asyncShowMessage(final IWorkbenchPart wpart, final String message, final boolean error) {
resultString = message;
if (!KbdMacroSupport.getInstance().suppressMessages()) {
super.asyncShowMessage(wpart,message,error);
}
}
/**
* @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 {
fileCompletions = null;
if (!KbdMacroSupport.getInstance().isDefining() && !KbdMacroSupport.getInstance().isExecuting()) {
String macroName = event.getParameter(NAME_ARG);
boolean forceIt = (event.getParameter(FORCE_ARG) != null ? Boolean.valueOf(event.getParameter(FORCE_ARG)) : false);
if (macroName != null && macroName.length() > 0) {
File file = macroFile(macroName);
if (file.exists()) {
loadMacro(editor,macroName,file,forceIt);
} else {
asyncShowMessage(editor, String.format(MACRO_MISSING, macroName), true);
}
} else {
mbState = this.nameState();
return mbState.run(editor);
}
} else {
asyncShowMessage(editor, BAD_MACRO_STATE, true);
}
return NO_OFFSET;
}
public SortedMap<String,?> getCompletions() {
if (fileCompletions == null) {
fileCompletions = getFileCompletions();
}
return fileCompletions;
}
/**
* Get state to handle prompt for getting kbd macro name
*
* @return naming prompt state
*/
protected IMinibufferState nameState() {
return new IMinibufferState() {
public String getMinibufferPrefix() {
return KBD_LOAD_PREFIX;
}
public int run(ITextEditor editor) {
miniTransform(new KbdMacroMinibuffer(KbdMacroLoadHandler.this),editor,null);
return NO_OFFSET;
}
public boolean executeResult(ITextEditor editor, Object minibufferResult) {
boolean result = true;
String name = (String) minibufferResult;
if (name == null || name.length() == 0) {
// no name entered
asyncShowMessage(editor, CANCEL, true);
}else {
File file = macroFile(name);
if (file.exists()) {
if (KbdMacroSupport.getInstance().getKbdMacro(name) != null) {
// macro by that name, request confirmation
transitionState(editor,name);
} else {
loadMacro(editor,name,file,false);
}
} else {
// no macro found
asyncShowMessage(editor, String.format(NO_NAME_UNO, name), true);
}
}
return result;
}
private void transitionState(ITextEditor editor, String name) {
mbState = yesnoState(name,MACRO_QUESTION, new IKbdMacroOperation() {
public void doOperation(ITextEditor editor, String name, File file) {
loadMacro(editor,name,file,false);
}
});
mbState.run(editor);
}
};
}
/**
* Load the macro from the File
* If the macro was saved with its binding, then restore that as well
* unless there is a conflict and the user decides against it
*
* @param editor
* @param name - the kbd macro's name
* @param file - the file where the keyboard macro resides
*/
private void loadMacro(ITextEditor editor, String name, File file, boolean forceIt) {
try {
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
KbdMacro kbdMacro = (KbdMacro) ois.readObject();
ois.close();
String keyString = kbdMacro.getBindingKeys();
final KeySequence sequence = (keyString != null ? KeySequence.getInstance(keyString) : null);
kbdMacro.setBindingKeys(null); // housekeeping: binding string is added on save
String badCommand = null;
if ((badCommand = checkMacro(editor, kbdMacro)) == null) {
final Command command = defineKbdMacro(editor,name);
// register loaded macro
KbdMacroSupport.getInstance().nameKbdMacro(name, kbdMacro);
// now see if we have a binding also
if (sequence != null) {
final String msg = String.format(LOADED_AND_BOUND, name, keyString);
final Binding oldBinding = checkForBinding(editor,sequence);
if (!forceIt && oldBinding != null) {
// macro's binding is already present, ask for advice
mbState = yesnoState(name,String.format(BINDING_QUESTION,keyString,FORMAT_S,FORMAT_S), new IKbdMacroOperation() {
public void doOperation(ITextEditor editor, String name, File file) {
bindMacro(editor,command,sequence,oldBinding);
asyncShowMessage(editor, msg, false);
}
});
mbState.run(editor);
} else {
bindMacro(editor,command,sequence,oldBinding);
asyncShowMessage(editor, msg, false);
}
} else {
asyncShowMessage(editor, String.format(LOADED, name), false);
}
} else {
asyncShowMessage(editor, String.format(BAD_CMD,badCommand), true);
}
} catch (Exception e) {
String msg = e.getMessage();
asyncShowMessage(editor, String.format(ABORT_LOAD, name, (msg != null ? msg : e.toString())), true);
}
}
/**
* Bind the loaded macro to its previous key binding, removing any conflicts
*
* @param editor
* @param command - the new kbd macro command
* @param sequence - key sequence for binding
* @param previous - conflicting binding
*/
private void bindMacro(ITextEditor editor, Command command, KeySequence sequence, Binding previous) {
if (command != null && sequence != null) {
IBindingService service = (editor != null) ? (IBindingService) editor.getSite().getService(IBindingService.class) :
(IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class);
if (service instanceof BindingService) {
BindingService bindingMgr = (BindingService) service;
if (previous != null) {
bindingMgr.removeBinding(previous);
}
ParameterizedCommand p = new ParameterizedCommand(command, null);
Binding binding = new KeyBinding(sequence, p,
KBD_SCHEMEID, KBD_CONTEXTID, null, null, null, Binding.USER);
bindingMgr.addBinding(binding);
// check for conflicts independent of the current Eclipse context
checkConflicts(bindingMgr,sequence,binding);
}
}
}
/**
* Check for binding conflicts independent of the current Eclipse context
* If the load is called from a non-editing context, any potential binding conflict will
* not be detected; so look for conflicts in a context independent set of bindings.
*
* @param service
* @param sequence
* @param binding
*/
private void checkConflicts(BindingService service, KeySequence sequence, Binding binding) {
Collection<Binding> conflicts = getTotalBindings().get(sequence);
if (conflicts != null) {
for (Binding conflict : conflicts) {
if (conflict != binding
&& binding.getContextId().equals(conflict.getContextId())
&& binding.getSchemeId().equals(conflict.getSchemeId())) {
service.removeBinding(conflict);
}
}
}
}
/**
* Verify statically that this macro will execute properly
* - Ensure the current Eclipse defines the commands used by the macro
*
* @param editor
* @param kbdMacro
* @return true if validates, else false
*/
private String checkMacro(ITextEditor editor, KbdMacro kbdMacro) {
String result = null;
ICommandService ics = (editor != null ) ? (ICommandService) editor.getSite().getService(ICommandService.class) :
(ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
@SuppressWarnings("unchecked") // Eclipse documents the type
Collection<String> cmdIds = (Collection<String>)ics.getDefinedCommandIds();
for (KbdEvent e : kbdMacro.getKbdMacro()) {
String cmdId;
if ((cmdId = e.getCmd()) != null) {
if (!cmdIds.contains(cmdId)) {
result = cmdId;
break;
}
}
}
return result;
}
}