/******************************************************************************* * Copyright (c) 2004, 2005 IBM Corporation and others. * 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 * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.gef.examples.text.tools; import java.util.ArrayList; import java.util.EventObject; import java.util.List; import java.util.Map; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ST; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Caret; import org.eclipse.jface.util.Assert; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.draw2d.Cursors; import org.eclipse.draw2d.UpdateListener; import org.eclipse.draw2d.UpdateManager; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.draw2d.text.CaretInfo; import org.eclipse.gef.DragTracker; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPartViewer; import org.eclipse.gef.GraphicalEditPart; import org.eclipse.gef.Request; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.CommandStackListener; import org.eclipse.gef.tools.SelectionTool; import org.eclipse.gef.tools.ToolUtilities; import org.eclipse.gef.examples.text.AppendableCommand; import org.eclipse.gef.examples.text.GraphicalTextViewer; import org.eclipse.gef.examples.text.SelectionRange; import org.eclipse.gef.examples.text.TextCommand; import org.eclipse.gef.examples.text.TextLocation; import org.eclipse.gef.examples.text.TextUtilities; import org.eclipse.gef.examples.text.actions.StyleListener; import org.eclipse.gef.examples.text.actions.StyleProvider; import org.eclipse.gef.examples.text.actions.StyleService; import org.eclipse.gef.examples.text.edit.TextEditPart; import org.eclipse.gef.examples.text.edit.TextStyleManager; import org.eclipse.gef.examples.text.requests.CaretRequest; import org.eclipse.gef.examples.text.requests.SearchResult; import org.eclipse.gef.examples.text.requests.TextRequest; /** * @since 3.1 */ public class TextTool extends SelectionTool implements StyleProvider { static final boolean IS_CARBON = "carbon".equals(SWT.getPlatform()); //$NON-NLS-1$ private static final int MODE_BS = 2; private static final int MODE_DEL = 3; private static final int MODE_TYPE = 1; private static final String KEY_OVERWRITE = "gef.texttool.overwrite"; //$NON-NLS-1$ private CommandStackListener commandListener = new CommandStackListener() { public void commandStackChanged(EventObject event) { fireStyleChanges(); discardCaretLocation(); } }; private StyleListener listener; private AppendableCommand pendingCommand; private ISelectionChangedListener selectionListener = new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { fireStyleChanges(); getCaret().setVisible(getSelectionRange() != null); queueCaretRefresh(true); } }; private UpdateListener updateListener = new UpdateListener() { public void notifyPainting(Rectangle damage, Map dirtyRegions) { queueCaretRefresh(false); } public void notifyValidating() { } }; private List styleKeys = new ArrayList(); // @TODO:Pratik StyleService cannot be final private final StyleService styleService; private List styleValues = new ArrayList(); private CaretRefresh caretRefresh; private int textInputMode, caretXLoc; private boolean isMirrored, xCaptured, overwrite; public TextTool() { this(null); } /** * @since 3.1 */ public TextTool(StyleService service) { styleService = service; } /* uncomment this when this class moves to the same package as AbstractTool * Need to override acceptAbort boolean acceptAbort(KeyEvent e) { return !isInState(STATE_INITIAL) && e.character == SWT.ESC; } */ public void addStyleListener(StyleListener listener) { Assert.isTrue(this.listener == null); this.listener = listener; } protected Cursor calculateCursor() { EditPart target = getTargetEditPart(); if (target instanceof TextEditPart) { TextEditPart textTarget = (TextEditPart)target; if (textTarget.acceptsCaret()) return Cursors.IBEAM; } return super.calculateCursor(); } private void recordCaretLocation() { if (!xCaptured) { caretXLoc = getCaretBounds().x; xCaptured = true; } } private void discardCaretLocation() { xCaptured = false; } /** * @since 3.1 * @param action * @param event */ private void doAction(int action, KeyEvent event) { boolean append = false; getUpdateManager().performUpdate(); setTextInputMode(0); event.doit = false; if (action == ST.PAGE_DOWN || action == ST.SELECT_PAGE_DOWN || action == ST.PAGE_UP || action == ST.SELECT_PAGE_UP || action == ST.SELECT_LINE_DOWN || action == ST.LINE_DOWN || action == ST.SELECT_LINE_UP || action == ST.LINE_UP) recordCaretLocation(); else discardCaretLocation(); switch (action) { case ST.SELECT_TEXT_START: append = true; case ST.TEXT_START: doSelect(CaretRequest.DOCUMENT, false, append, null); break; case ST.SELECT_TEXT_END: append = true; case ST.TEXT_END: doSelect(CaretRequest.DOCUMENT, true, append, null); break; case ST.SELECT_PAGE_DOWN: append = true; case ST.PAGE_DOWN: doTraversePage(true, append); break; case ST.SELECT_PAGE_UP: append = true; case ST.PAGE_UP: doTraversePage(false, append); break; case ST.DELETE_PREVIOUS: doBackspace(); break; case ST.DELETE_NEXT: doDelete(); break; //HOME case ST.SELECT_LINE_START: append = true; case ST.LINE_START: doSelect(CaretRequest.LINE_BOUNDARY, false, append, null); break; // WORD_PREV case ST.SELECT_WORD_NEXT: append = true; case ST.WORD_NEXT: doSelect(CaretRequest.WORD_BOUNDARY, true, append, null); break; // WORD_NEXT case ST.SELECT_WORD_PREVIOUS: append = true; case ST.WORD_PREVIOUS: doSelect(CaretRequest.WORD_BOUNDARY, false, append, null); break; //END case ST.SELECT_LINE_END: append = true; case ST.LINE_END: doSelect(CaretRequest.LINE_BOUNDARY, true, append, null); break; //LEFT case ST.SELECT_COLUMN_PREVIOUS: append = true; case ST.COLUMN_PREVIOUS: doSelect(CaretRequest.COLUMN, false, append, null); break; //RIGHT case ST.SELECT_COLUMN_NEXT: append = true; case ST.COLUMN_NEXT: doSelect(CaretRequest.COLUMN, true, append, null); break; //UP case ST.SELECT_LINE_UP: append = true; case ST.LINE_UP: doSelect(CaretRequest.ROW, false, append, null); break; //DOWN case ST.SELECT_LINE_DOWN: append = true; case ST.LINE_DOWN: doSelect(CaretRequest.ROW, true, append, null); break; // WINDOW END case ST.SELECT_WINDOW_END: append = true; case ST.WINDOW_END: doSelect(CaretRequest.WINDOW, true, append, null); break; // WINDOW START case ST.SELECT_WINDOW_START: append = true; case ST.WINDOW_START: doSelect(CaretRequest.WINDOW, false, append, null); break; case ST.TOGGLE_OVERWRITE: toggleOverwrite(); break; //TAB case SWT.TAB | SWT.SHIFT: doUnindent(); break; case SWT.TAB: if (!doIndent()) doTyping(event); break; //ENTER case SWT.CR: if (!doNewline()) doTyping(event); break; default: event.doit = true; break; } } /** * @since 3.1 * @param e */ private boolean doBackspace() { setTextInputMode(MODE_BS); SelectionRange range = getSelectionRange(); if (range.isEmpty()) { if (handleTextEdit(new TextRequest(TextRequest.REQ_BACKSPACE, range, pendingCommand))) return true; doSelect(CaretRequest.COLUMN, false, false, null); return false; } else return handleTextEdit(new TextRequest(TextRequest.REQ_REMOVE_RANGE, range)); } private boolean doDelete() { setTextInputMode(MODE_DEL); SelectionRange range = getSelectionRange(); if (range.isEmpty()) { if (handleTextEdit(new TextRequest(TextRequest.REQ_DELETE, range, pendingCommand))) return true; doSelect(CaretRequest.COLUMN, true, false, null); return false; } else return handleTextEdit(new TextRequest(TextRequest.REQ_REMOVE_RANGE, range)); } /** * @since 3.1 */ private boolean doIndent() { setTextInputMode(0); SelectionRange range = getSelectionRange(); TextRequest edit; if (range.isEmpty()) edit = new TextRequest(TextRequest.REQ_INDENT, range); else return false; return handleTextEdit(edit); } /** * @since 3.1 * @param e */ private boolean doInsertContent(char c) { setTextInputMode(MODE_TYPE); TextRequest edit = new TextRequest( overwrite ? TextRequest.REQ_OVERWRITE : TextRequest.REQ_INSERT, getSelectionRange(), Character.toString(c), pendingCommand); String keys[] = new String[styleKeys.size()]; styleKeys.toArray(keys); edit.setStyles(keys, styleValues.toArray()); return handleTextEdit(edit); } /** * @since 3.1 * @param e */ private void doKeyDown(KeyEvent event) { int action = 0; if (event.keyCode != 0) { action = lookupAction(event.keyCode | event.stateMask); } else { action = lookupAction(event.character | event.stateMask); if (action == 0) { // see if we have a control character if ((event.stateMask & SWT.CTRL) != 0 && (event.character >= 0) && event.character <= 31) { // get the character from the CTRL+char sequence, the control // key subtracts 64 from the value of the key that it modifies int c = event.character + 64; action = lookupAction(c | event.stateMask); } } } if (action == 0) doTyping(event); else doAction(action, event); } /** * @since 3.1 */ private boolean doNewline() { setTextInputMode(MODE_BS); SelectionRange range = getSelectionRange(); TextRequest edit; Assert.isTrue(range.isEmpty()); edit = new TextRequest(TextRequest.REQ_NEWLINE, range, pendingCommand); return handleTextEdit(edit); } private void doSelect(Object type, boolean isForward, boolean append, Point loc) { GraphicalTextViewer viewer = getTextualViewer(); SearchResult result = new SearchResult(); CaretRequest search = new CaretRequest(); search.setType(type); search.isForward = isForward; search.setLocation(loc); SelectionRange range = getSelectionRange(); if (range == null) { if (viewer.getContents() instanceof TextEditPart) { TextEditPart tep = (TextEditPart)viewer.getContents(); if (tep.acceptsCaret()) tep.getTextLocation(search, result); } } else { TextLocation caretLocation = getCaretLocation(); if (loc == null) search.setLocation(new Point(xCaptured ? caretXLoc : getCaretBounds().x, getCaretInfo().getBaseline())); search.where = caretLocation; caretLocation.part.getTextLocation(search, result); // isForward = range.isForward; } if (result.location == null) return; if (append) { TextLocation otherEnd = isForward ? range.begin : range.end; if (TextUtilities.isForward(otherEnd, result.location)) range = new SelectionRange(otherEnd, result.location, true, result.trailing); else range = new SelectionRange(result.location, otherEnd, false, result.trailing); viewer.setSelectionRange(range); } else viewer.setSelectionRange(new SelectionRange( result.location, result.location, isForward, result.trailing)); } private void doTraversePage(boolean isForward, boolean appendSelection) { Rectangle caretBounds = getCaretBounds(); Point loc = caretBounds.getCenter(); loc.x = caretXLoc; int viewerHeight = getTextualViewer().getControl().getBounds().height - caretBounds.height; if (isForward) loc.y += viewerHeight; else loc.y -= viewerHeight; doSelect(CaretRequest.LOCATION, isForward, appendSelection, loc); } /** * @since 3.1 * @param event */ private void doTyping(KeyEvent event) { boolean ignore = false; discardCaretLocation(); if (IS_CARBON) { // Ignore accelerator key combinations (we do not want to // insert a character in the text in this instance). Do not // ignore COMMAND+ALT combinations since that key sequence // produces characters on the mac. ignore = (event.stateMask ^ SWT.COMMAND) == 0 || (event.stateMask ^ (SWT.COMMAND | SWT.SHIFT)) == 0; } else { // Ignore accelerator key combinations (we do not want to // insert a character in the text in this instance). Don't // ignore CTRL+ALT combinations since that is the Alt Gr // key on some keyboards. ignore = (event.stateMask ^ SWT.ALT) == 0 || (event.stateMask ^ SWT.CTRL) == 0 || (event.stateMask ^ (SWT.ALT | SWT.SHIFT)) == 0 || (event.stateMask ^ (SWT.CTRL | SWT.SHIFT)) == 0; } // -ignore anything below SPACE except for line delimiter keys and tab. // -ignore DEL if (!ignore && event.character > 31 && event.character != SWT.DEL || event.character == SWT.CR || event.character == SWT.LF || event.character == '\t') { doInsertContent(event.character); event.doit = false; } } private boolean doUnindent() { setTextInputMode(0); SelectionRange range = getSelectionRange(); TextRequest edit; if (range.isEmpty()) edit = new TextRequest(TextRequest.REQ_UNINDENT, range); else return false; return handleTextEdit(edit); } private void fireStyleChanges() { if (listener != null) listener.styleChanged(null); } private void flushStyles() { styleKeys.clear(); styleValues.clear(); } private Caret getCaret() { Caret caret = null; if (getCurrentViewer() != null) { Canvas canvas = (Canvas)getCurrentViewer().getControl(); caret = canvas.getCaret(); if (caret == null) caret = new Caret(canvas, 0); } return caret; } public Rectangle getCaretBounds() { return new Rectangle(getCaret().getBounds()); } public CaretInfo getCaretInfo() { TextLocation location = getCaretLocation(); return location.part.getCaretPlacement(location.offset, getSelectionRange().trailing); } public TextLocation getCaretLocation() { if (getSelectionRange().isForward) return getSelectionRange().end; return getSelectionRange().begin; } public TextEditPart getCaretOwner() { if (getSelectionRange() != null) return getCaretLocation().part; return null; } private SelectionRange getSelectionRange() { if (getCurrentViewer() instanceof GraphicalTextViewer) return getTextualViewer().getSelectionRange(); return null; } private UpdateManager getUpdateManager() { EditPartViewer viewer = getCurrentViewer(); if (viewer != null) { EditPart root = viewer.getRootEditPart(); if (root instanceof GraphicalEditPart) return ((GraphicalEditPart)root).getFigure().getUpdateManager(); } return null; } private Object getSelectionStyle(String styleID, boolean isState) { TextRequest req = new TextRequest(TextRequest.REQ_STYLE, getSelectionRange()); req.setStyles(new String[] {styleID}, new Object[] {null}); EditPart target = getTextTarget(req); if (target == null) return StyleService.UNDEFINED; TextStyleManager manager = (TextStyleManager)target .getAdapter(TextStyleManager.class); if (isState) return manager.getStyleState(styleID, getSelectionRange()); return manager.getStyleValue(styleID, getSelectionRange()); } public Object getStyle(String styleID) { for (int i = 0; i < styleKeys.size(); i++) if (styleID.equals(styleKeys.get(i))) return styleValues.get(i); return getSelectionStyle(styleID, false); } public Object getStyleState(String styleID) { return getSelectionStyle(styleID, true); } private TextEditPart getTextTarget(Request request) { SelectionRange range = getSelectionRange(); if (range == null) return null; EditPart target, candidate = ToolUtilities.findCommonAncestor(range.begin.part, range.end.part); do { target = candidate.getTargetEditPart(request); candidate = candidate.getParent(); } while (target == null && candidate != null); return (TextEditPart)target; } GraphicalTextViewer getTextualViewer() { return (GraphicalTextViewer)getCurrentViewer(); } protected boolean handleButtonDown(int button) { discardCaretLocation(); return super.handleButtonDown(button); } protected boolean handleCommandStackChanged() { setTextInputMode(0); discardCaretLocation(); return super.handleCommandStackChanged(); } protected boolean handleFocusGained() { if (getSelectionRange() == null) doSelect(CaretRequest.DOCUMENT, false, false, null); return super.handleFocusGained(); } protected boolean handleKeyDown(KeyEvent e) { if (isInState(STATE_INITIAL) && getTextualViewer().isTextSelected()) doKeyDown(e); if (e.doit) return super.handleKeyDown(e); return true; } protected void handleKeyTraversed(TraverseEvent event) { if ((event.detail == SWT.TRAVERSE_TAB_PREVIOUS || event.detail == SWT.TRAVERSE_TAB_NEXT) && (event.stateMask & SWT.CTRL) == 0) event.doit = false; } protected boolean handleMove() { super.handleMove(); refreshCursor(); return true; } private boolean handleTextEdit(TextRequest edit) { GraphicalTextViewer viewer = getTextualViewer(); EditPart target = getTextTarget(edit); Command insert = null; if (target != null) insert = target.getCommand(edit); if (insert == null) return false; if (pendingCommand == null || insert != pendingCommand) { if (!insert.canExecute()) return false; executeCommand(insert); if (insert instanceof AppendableCommand) pendingCommand = (AppendableCommand)insert; else pendingCommand = null; } else { if (!pendingCommand.canExecutePending()) return false; pendingCommand.executePending(); viewer.setSelectionRange(((TextCommand)pendingCommand).getExecuteSelectionRange(viewer)); } return true; } /** * @since 3.1 * @param i * @return */ private int lookupAction(int i) { switch (i) { //Left and Right case SWT.ARROW_LEFT: return isMirrored ? ST.COLUMN_NEXT : ST.COLUMN_PREVIOUS; case SWT.ARROW_RIGHT: return isMirrored ? ST.COLUMN_PREVIOUS : ST.COLUMN_NEXT; case SWT.ARROW_RIGHT | SWT.SHIFT: return isMirrored ? ST.SELECT_COLUMN_PREVIOUS : ST.SELECT_COLUMN_NEXT; case SWT.ARROW_LEFT | SWT.SHIFT: return isMirrored ? ST.SELECT_COLUMN_NEXT : ST.SELECT_COLUMN_PREVIOUS; case SWT.ARROW_RIGHT | SWT.CONTROL: return isMirrored ? ST.WORD_PREVIOUS : ST.WORD_NEXT; case SWT.ARROW_RIGHT | SWT.CONTROL | SWT.SHIFT: return isMirrored ? ST.SELECT_WORD_PREVIOUS : ST.SELECT_WORD_NEXT; case SWT.ARROW_LEFT| SWT.CONTROL: return isMirrored ? ST.WORD_NEXT : ST.WORD_PREVIOUS; case SWT.ARROW_LEFT| SWT.CONTROL | SWT.SHIFT: return isMirrored ? ST.SELECT_WORD_NEXT : ST.SELECT_WORD_PREVIOUS; case ST.LINE_END: case ST.TOGGLE_OVERWRITE: case ST.SELECT_LINE_END: case ST.LINE_START: case ST.SELECT_LINE_START: case ST.PAGE_UP: case ST.PAGE_DOWN: case ST.SELECT_PAGE_UP: case ST.SELECT_PAGE_DOWN: case ST.LINE_UP: case ST.LINE_DOWN: case ST.SELECT_LINE_UP: case ST.SELECT_LINE_DOWN: case ST.TEXT_END: case ST.SELECT_TEXT_END: case ST.TEXT_START: case ST.SELECT_TEXT_START: case ST.DELETE_PREVIOUS: case ST.DELETE_NEXT: case ST.WINDOW_START: case ST.WINDOW_END: case ST.SELECT_WINDOW_START: case ST.SELECT_WINDOW_END: case SWT.TAB | SWT.SHIFT: case SWT.TAB: return i; case SWT.LF: case SWT.CR: return SWT.CR; default: break; } return 0; } void queueCaretRefresh(boolean revealAfterwards) { if (!getCaret().isVisible()) return; if (caretRefresh == null) { caretRefresh = new CaretRefresh(revealAfterwards); getUpdateManager().runWithUpdate(caretRefresh); } else caretRefresh.enableReveal(revealAfterwards); } public void removeStyleListener(StyleListener listener) { Assert.isTrue(this.listener == listener); this.listener = null; } public void setDragTracker(DragTracker newDragTracker) { if (getDragTracker() == newDragTracker) return; setTextInputMode(0); super.setDragTracker(newDragTracker); } public void setStyle(String styleID, Object newValue) { //Check for cancellations: lookup old style and remove any pending ones Object oldValue = getSelectionStyle(styleID, false); if (newValue.equals(oldValue)) { int prev = styleKeys.indexOf(styleID); if (prev != - 1) { styleKeys.remove(prev); styleValues.remove(prev); return; } } //Try to apply immediately, pend otherwise. TextRequest req = new TextRequest(TextRequest.REQ_STYLE, getSelectionRange()); //$TODO should this be all pending styles or just the recently set? req.setStyles(new String[] {styleID}, new Object[] {newValue}); EditPart target = getTextTarget(req); Command c = target.getCommand(req); if (c == null) { int prev = styleKeys.indexOf(styleID); if (prev != - 1) { styleKeys.remove(prev); styleValues.remove(prev); } styleKeys.add(0, styleID); styleValues.add(0, newValue); } else if (c.canExecute()) { //$TODO cleanup any pending styles? executeCommand(c); } } public void setViewer(EditPartViewer viewer) { EditPartViewer currentViewer = getCurrentViewer(); if (viewer == currentViewer || viewer == null) return; if (currentViewer != null) { if (caretRefresh != null) getUpdateManager().performUpdate(); currentViewer.getEditDomain().getCommandStack() .removeCommandStackListener(commandListener); currentViewer.removeSelectionChangedListener(selectionListener); UpdateManager manager = getUpdateManager(); if (manager != null) manager.removeUpdateListener(updateListener); currentViewer.setProperty(KEY_OVERWRITE, overwrite ? Boolean.TRUE : Boolean.FALSE); if (styleService != null) styleService.setStyleProvider(null); setTextInputMode(0); setTargetRequest(null); } super.setViewer(viewer); if (viewer != null) { isMirrored = (viewer.getControl().getStyle() & SWT.MIRRORED) != 0; viewer.getEditDomain().getCommandStack().addCommandStackListener(commandListener); viewer.addSelectionChangedListener(selectionListener); UpdateManager manager = getUpdateManager(); if (manager != null) manager.addUpdateListener(updateListener); Boolean bool = (Boolean)viewer.getProperty(KEY_OVERWRITE); overwrite = bool != null && bool.booleanValue(); if (styleService != null) styleService.setStyleProvider(this); } } /** * @since 3.1 * @param mode the new input mode */ private void setTextInputMode(int mode) { if (textInputMode != mode) pendingCommand = null; if (textInputMode != MODE_TYPE) flushStyles(); textInputMode = mode; } private void toggleOverwrite() { overwrite = !overwrite; queueCaretRefresh(false); } class CaretRefresh implements Runnable { private boolean reveal; public CaretRefresh(boolean reveal) { enableReveal(reveal); } public void run() { refreshCaret(); caretRefresh = null; if (reveal) getTextualViewer().revealCaret(); } public void refreshCaret() { if (getCaretOwner() == null) return; CaretInfo info = getCaretInfo(); getCaret().setBounds(info.getX(), info.getY(), overwrite ? info.getHeight() / 2 : 1, info.getHeight()); } public void enableReveal(boolean newVal) { reveal |= newVal; } } }