/** * 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.minibuffer; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.text.TextEvent; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds; import com.mulgasoft.emacsplus.execute.IBindingResult; import com.mulgasoft.emacsplus.execute.IUniversalResult; import org.eclipse.jface.bindings.keys.KeySequence; /** * Read a number and execute a subsequent command n times * * @author Mark Feber - initial API and implementation */ public class UniversalMinibuffer extends KeyHandlerMinibuffer { /* * C-u runs `universal-argument' * * Begin a numeric argument for the following command. * * Digits or minus sign following C-u make up the numeric argument. C-u following * the digits or minus sign ends the argument. C-u without digits or minus * sign provides 4 as argument. Repeating C-u without digits or minus sign * multiplies the argument by 4 each time. */ private static int MULTIPLIER = 4; private int argumentCount = 4; private StringBuilder countBuf; private StringBuilder prefixBuf; private String uprefix; private final static String CPREFIX = "C-"; //$NON-NLS-1$ private final static String MPREFIX = "M-"; //$NON-NLS-1$ private int triggerCount = 0; private int triggerMask = 0; private char triggerChar = 0x15; // default to ^U private boolean restart = false; private boolean executed = false; private int uTriggerMask = SWT.CTRL; // universal digit-argument reset private char uTriggerChar = 0x15; // universal digit-argument reset (^U) /** * @param executable */ public UniversalMinibuffer(IMinibufferExecutable executable) { super(executable); } private void setArgumentCount(int count) { argumentCount = count; } private boolean isMinus(Event event) { // OS X has a different character for ^-, so also check keyCode return ('-' == event.character || 45 == event.keyCode); } private boolean isMinus(VerifyEvent event) { // OS X has a different character for ^-, so also check keyCode return ('-' == event.character || 45 == event.keyCode); } /** * @see com.mulgasoft.emacsplus.minibuffer.KeyHandlerMinibuffer#beginSession(ITextEditor, IWorkbenchPage, ExecutionEvent) */ @Override public boolean beginSession(ITextEditor editor, IWorkbenchPage page, ExecutionEvent event) { countBuf = new StringBuilder(); Object eTrigger = event.getTrigger(); if (eTrigger instanceof Event) { char c = ((Event)eTrigger).character; if (c != 0) { triggerMask = ((Event)eTrigger).stateMask; if (Character.isDigit(c)) { countBuf.append(c); } else if (isMinus((Event)eTrigger)) { countBuf.append('-'); setArgumentCount(-1); } else { // remember non-numeric character that launched us triggerChar = c; } } } executed = false; triggerCount = 0; prefixBuf = new StringBuilder(); // The handler sets the prefix based on the key invocation uprefix = getExecutable().getMinibufferPrefix().trim(); return super.beginSession(editor, page, event); } /** * @see com.mulgasoft.emacsplus.minibuffer.KeyHandlerMinibuffer#charEvent(org.eclipse.swt.events.VerifyEvent) */ @Override protected void charEvent(VerifyEvent event) { event.doit = false; if (!hasTrigger()) { // accumulate multiple u-a invocations, or reset on delayed repeat if (isUniversalKey(event)) { processUniversal(); } else if (!restart && (Character.isDigit(event.character) && ((event.stateMask & SWT.MODIFIER_MASK)== 0 || event.stateMask == triggerMask))) { // if plain number // build up the number string setArgumentCount(1); addToCount(event.character); } else if (!restart && isMinus(event)) { resetToMinus(event.keyCode, event.stateMask); } else { // else building trigger sequence addToTrigger(event); } } else { // continue with trigger sequence addToTrigger(event); } updatePrefix(); } /** * @param event */ private void resetToMinus(int keyCode, int stateMask) { countBuf = new StringBuilder(); countBuf.append('-'); setArgumentCount(-1); resetPrefix(); updatePrefix(this.getTrigger(keyCode, stateMask)); } private boolean isUniversalKey(VerifyEvent event) { boolean result = false; if (result= (event.character == triggerChar && event.stateMask == triggerMask)) { if (triggerChar == '-') { uprefix = getTrigger(event.keyCode,event.stateMask); result = false; } else if (result = (event.character == uTriggerChar && (event.stateMask & SWT.MODIFIER_MASK) == uTriggerMask)) { // the universal reset value uprefix = getTrigger(event.keyCode,event.stateMask); } } else if (triggerMask == 0) { // likely invoked by M-x universal-argument or from kbd macro if (result = (event.character == uTriggerChar && (event.stateMask & SWT.MODIFIER_MASK) == uTriggerMask)){ updatePrefix(' '); } } return result; } /** * On universal key detection, process appropriately depending on state of count, etc. */ private void processUniversal() { String nextPrefix = uprefix; if (restart) { // reset and restart count accumulation restart = false; countBuf = new StringBuilder(); } if (countBuf.length() == 0) { setArgumentCount(argumentCount * MULTIPLIER); } else if (countBuf.length() == 1 && countBuf.charAt(0) == '-') { setArgumentCount(argumentCount * MULTIPLIER); // remove from buffer, so it will reset on integer argument countBuf.deleteCharAt(0); } else { // flag an end to numeric input - Emacs restarts the count at 1 if yet another C-u appears restart = true; setArgumentCount(1); updatePrefix(' '); } // add universal prefix to minibuffer prefix updatePrefix(nextPrefix); } private void addToCount(char number) { // add number countBuf.append(number); updatePrefix(number); } private void addToTrigger(VerifyEvent event) { triggerCount++; super.charEvent(event); } private void updatePrefix() { if (!executed) { setMinibufferPrefix(getCommandString()); initMinibuffer(EMPTY_STR); } } private void updatePrefix(String nextfix) { updatePrefix(nextfix, true); } private void updatePrefix(String nextfix, boolean isString) { prefixBuf.append(nextfix); if (isString) { prefixBuf.append(' '); } } private void updatePrefix(char nextfix) { updatePrefix(normalizeChar(nextfix), false); } private String getCommandString() { return (prefixBuf.toString() + ((getTrigger() == null) ? EMPTY_STR : ' ' + getTrigger().format().trim())); } private void resetPrefix() { prefixBuf = new StringBuilder(); } /** * Adapt the prefix display in the minibuffer to the actual key binding * * @param keyCode * @param stateMask * @return the trigger string */ public String getTrigger(int keyCode, int stateMask) { StringBuilder result = new StringBuilder(); String c = new String(Character.toChars(keyCode)); switch (stateMask) { case SWT.CTRL: result.append(CPREFIX); result.append(c); break; case SWT.ALT: result.append(MPREFIX); result.append(c); break; case SWT.CTRL|SWT.ALT: result.append(CPREFIX); result.append(MPREFIX); result.append(c); break; default: result.append(KeyStroke.getInstance(stateMask,keyCode).format()); } if (result.length() > 0) { result.append(' '); } return result.toString(); } /** * @see com.mulgasoft.emacsplus.minibuffer.ExecutingMinibuffer#executeResult(org.eclipse.ui.texteditor.ITextEditor, java.lang.Object) */ @Override protected boolean executeResult(ITextEditor editor, Object commandResult) { boolean result = true; String resultString = getCommandString(); setResultString(resultString, false); setResultMessage(resultString, false, true); try { setExecuting(true); result = exitExecuteResult(editor, commandResult); } finally { setExecuting(false); executed = true; } return result; } /** * @see com.mulgasoft.emacsplus.minibuffer.KeyHandlerMinibuffer#getResult(Binding, KeySequence, String) */ @Override protected IBindingResult getResult(final Binding binding, final KeySequence trigger, String triggerString) { // key character is only > 0 if it is stand alone int charpoint = getKeyCharacter(); String character = null; if (binding == null && charpoint > 0 && triggerCount < 2) { if (charpoint == SWT.CR || charpoint == SWT.LF) { character = getEol(); } else if (charpoint == SWT.BS) { character = new String(Character.toChars(charpoint)); } else if ((Character.isWhitespace(charpoint)) || (charpoint > ' ')) { character = new String(Character.toChars(charpoint)); } } if (countBuf.length() > 0) { try { if (countBuf.length() == 1 && countBuf.charAt(0)== '-') { ; // just use argument Count } else { setArgumentCount(Integer.parseInt(countBuf.toString())); } } catch (NumberFormatException e) { // bad count setArgumentCount(1); } } final String key = character; final boolean notNumeric = countBuf.length() == 0 && argumentCount == 4; // flag whether a number was entered into the minibuffer return new IUniversalResult() { public Binding getKeyBinding() { return binding; } public String getKeyString() { return key; } public int getCount() { return argumentCount; } public boolean isNumeric() { return !notNumeric; } public KeySequence getTrigger() { return trigger; } }; } // ISelectionChangedListener /* (non-Javadoc) * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#selectionChanged(org.eclipse.jface.viewers.SelectionChangedEvent) */ public void selectionChanged(SelectionChangedEvent event) { // During execution of sub-command, selection can change if (!isExecuting()) { super.selectionChanged(event); } } // ITextListener /* (non-Javadoc) * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#textChanged(org.eclipse.jface.text.TextEvent) */ public void textChanged(TextEvent event) { // During execution of sub-command, text can change if (!isExecuting()) { super.textChanged(event); } } // Support simple Alt re-binding of universal-argument private Traverser universalTraverser = new Traverser(); /* (non-Javadoc) * @see com.mulgasoft.emacsplus.Minibuffer#addOtherListeners(org.eclipse.jface.text.source.ISourceViewer, org.eclipse.swt.custom.StyledText) */ protected void addOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) { Display.getCurrent().addFilter(SWT.Traverse, universalTraverser); } /* (non-Javadoc) * @see com.mulgasoft.emacsplus.Minibuffer#removeOtherListeners(org.eclipse.jface.text.source.ISourceViewer, org.eclipse.swt.custom.StyledText) */ protected void removeOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) { Display.getCurrent().removeFilter(SWT.Traverse, universalTraverser); } private boolean isUniversalBinding() { boolean result = false; Binding bind = getBinding(); if (bind != null) { Command cmd = bind.getParameterizedCommand().getCommand(); result = (cmd != null && cmd.getId().equals(IEmacsPlusCommandDefinitionIds.UNIVERSAL_ARGUMENT)); } return result; } private class Traverser implements Listener { public void handleEvent(Event event) { boolean check = checkKey(event.stateMask, event.keyCode, event.character); if (check && isUniversalBinding()) { // clear key cache resetKeys(); // check for binding of the form M-1 etc. if (Character.isDigit(event.character)) { addToCount(event.character); } else if (isMinus(event)) { resetToMinus(event.keyCode, event.stateMask); } else { // reset count processUniversal(); } updatePrefix(); } else if (processKey(check,event.keyCode)) { leave(); } // setting detail to NONE but doit=true disables further processing event.type = SWT.None; event.detail = SWT.TRAVERSE_NONE; event.doit = true; } } }