/** * Copyright (c) 2009, 2013 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.jface.text.source.ISourceViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.IWorkbenchPage; import com.mulgasoft.emacsplus.RingBuffer; import com.mulgasoft.emacsplus.YankRotate; import com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement; /** * Support a minibuffer with RingBuffer based history list * * @author Mark Feber - initial API and implementation */ public abstract class HistoryMinibuffer extends WithMinibuffer implements TraverseListener { // TODO preferences? static final int NEXT = 'n'; static final int PREV = 'p'; static final int NEXT_ARROW = SWT.ARROW_DOWN; static final int PREV_ARROW = SWT.ARROW_UP; // For issues with non-generic method overriding a generic one see: // http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ823 /** * Get the history ring buffer for this minibuffer * @return RingBuffer instance */ protected abstract <T> RingBuffer<T> getHistoryRing(); /** * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#handlesAlt() */ @Override protected boolean handlesAlt() { return true; } /** * Handle the history navigation commands (via arrow keys) * * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#noCharEvent(org.eclipse.swt.events.VerifyEvent) */ protected void noCharEvent(VerifyEvent event) { switch (event.keyCode) { case NEXT_ARROW: historyChange(event,YankRotate.BACKWARD); event.doit = false; break; case PREV_ARROW: historyChange(event,YankRotate.FORWARD); event.doit = false; break; default: super.noCharEvent(event); break; } } /** * Handle the history navigation commands (via Meta) * * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#dispatchAlt(org.eclipse.swt.events.VerifyEvent) */ protected boolean dispatchAlt(VerifyEvent event) { boolean result = false; switch (event.keyCode) { case NEXT: // search for the next item in the search ring. historyChange(event,YankRotate.BACKWARD); result = true; break; case PREV: // search for the previous item in the search ring. historyChange(event,YankRotate.FORWARD); result = true; break; // TODO - window captures Alt-SPACE first /* case ' ': // space completion if (isCompleting()) { showCompletions(); result = true; break; } // TODO - window captures Alt-TAB first case '\t': // complete the search string using the search ring. System.out.println("Alt-TAB: complete the search string using the search ring."); break; */ default: result = super.dispatchAlt(event); } return result; } /** * Update the minibuffer string with the history entry * * @param event the event provoking the history change * @param dir the direction in which to rotate the history ring */ private void historyChange(VerifyEvent event, YankRotate dir) { // don't start rotating through history unless the preceding key was a history command replaceFromHistory(wasHistKey(event) ? getHistoryRing().rotateYankPos(dir) : getHistoryRing().yankElement()); historyTransition(event.keyCode); } /** * Initialize the minibuffer with the string from the yanked RingBuffer element */ protected void replaceFromHistory() { replaceFromHistory(getHistoryRing().yankElement()); } /** * Initialize the minibuffer with the string from the RingBuffer element * * @param rbe */ protected void replaceFromHistory(IRingBufferElement<?> rbe) { if (rbe != null) { String historyStr = rbe.toString(); if (historyStr != null) { initMinibuffer(historyStr); } } } /** * Hook method for subclasses to add behavior when the entry has changed via a history key * Default does nothing. * * @param keyCode the key that provoked the transition */ protected void historyTransition (int keyCode) { // default do nothing } private boolean wasHistKey(VerifyEvent event) { int last = getLastKeyCode(); return (last == NEXT || last == PREV || last == NEXT_ARROW || last == PREV_ARROW); } protected <T> IRingBufferElement<T> addToHistory(T history) { IRingBufferElement<T> result = null; if (history != null && history.toString().length() > 0) { RingBuffer<T> hist = getHistoryRing(); // check against (possible) rotate position and bottom and top position if (hist != null && (hist.isEmpty() || !hist.isDuplicate(history))) { result = hist.putNext(history); } } return result; } /** * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#addOtherListeners(IWorkbenchPage, ISourceViewer, StyledText) */ @Override protected void addOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) { if (handlesAlt()) { widget.addTraverseListener(this); } } /** * @see com.mulgasoft.emacsplus.minibuffer.WithMinibuffer#removeOtherListeners(IWorkbenchPage, ISourceViewer, StyledText) */ @Override protected void removeOtherListeners(IWorkbenchPage page, ISourceViewer viewer, StyledText widget) { if (handlesAlt()) { widget.removeTraverseListener(this); } } // TraverseListener // Disable mnemonic traversal for M-p & M-n public void keyTraversed(TraverseEvent e) { switch (e.detail) { case SWT.TRAVERSE_MNEMONIC: if ((e.stateMask & SWT.MOD3) != 0) { switch (e.character) { case PREV: case NEXT: // if no key binding (which would take precedence) // then pass to our handlers it directly if (!hasBinding(e)){ handleTraverseEvent(e); } } } break; default: } } /** * Disable eclipse traversal event, and dispatch into our Alt/Ctrl * handlers in place of it * * @param e the trapped TraverseEvent */ protected void handleTraverseEvent(TraverseEvent e) { // setting detail to NONE but doit=true disables further processing e.detail = SWT.TRAVERSE_NONE; e.doit = true; Event ee = new Event(); ee.character = e.character; ee.doit = true; ee.stateMask = (e.stateMask & SWT.MODIFIER_MASK); ee.keyCode = e.keyCode; ee.display = e.display; ee.widget = e.widget; // will throw an exception if not valid ee.time = e.time; ee.data = e.data; switch (ee.stateMask) { case SWT.CONTROL: // Emacs+ key binding forces CTRL dispatchCtrl(new VerifyEvent(ee)); break; case SWT.ALT: // AFAIK MOD3 is always ALT dispatchAlt(new VerifyEvent(ee)); break; } } }