/** * Copyright 2004-2016 Riccardo Solmi. All rights reserved. * This file is part of the Whole Platform. * * The Whole Platform is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The Whole Platform is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the Whole Platform. If not, see <http://www.gnu.org/licenses/>. */ package org.whole.lang.e4.ui.actions; import static org.eclipse.draw2d.PositionConstants.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.draw2d.FigureUtilities; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.PositionConstants; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Point; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.gef.ConnectionEditPart; import org.eclipse.gef.EditPart; import org.eclipse.gef.GraphicalEditPart; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; import org.whole.lang.ui.editor.IGEFEditorKit; import org.whole.lang.ui.editparts.IEntityPart; import org.whole.lang.ui.editparts.IGraphicalEntityPart; import org.whole.lang.ui.editparts.ITextualEntityPart; import org.whole.lang.ui.figures.ITextualFigure; import org.whole.lang.ui.keys.IKeyHandler; import org.whole.lang.ui.tools.EditPoint; import org.whole.lang.ui.tools.IEditPointProvider; import org.whole.lang.ui.tools.Tools; import org.whole.lang.ui.util.CaretUpdater; import org.whole.lang.ui.util.CaretUtils; /** * @author Riccardo Solmi, Enrico Persiani */ public class E4NavigationKeyHandler extends E4KeyHandler implements IEditPointProvider { public E4NavigationKeyHandler(IEclipseContext context) { super(context); } /** * Returns the list of editparts which are conceptually at the same level of navigation as * the currently focused editpart. By default, this is the siblings of the focused part. * @return a list of navigation editparts */ public List<?> getNavigationSiblings() { return getFocusEntityPart().getParent().getChildren(); } boolean insertMode = true; public boolean isInsertMode() { return insertMode; } public void toggleInsertMode() { insertMode = !insertMode; } /** * Returns the cached node. It is possible that the node is not longer in the viewer but * has not been garbage collected yet. */ private WeakReference<GraphicalEditPart> cachedNode; private GraphicalEditPart getCachedNode() { if (cachedNode == null) return null; if (cachedNode.isEnqueued()) return null; return (GraphicalEditPart)cachedNode.get(); } private void setCachedNode(GraphicalEditPart node) { if (node == null) cachedNode = null; else cachedNode = new WeakReference<GraphicalEditPart>(node); } /** * Given a connection on a node, this method finds the next (or the previous) connection * of that node. * * @param node The EditPart whose connections are being traversed * @param current The connection relative to which the next connection has to be found * @param forward <code>true</code> if the next connection has to be found; false otherwise */ private int counter = 0; @SuppressWarnings("unchecked") ConnectionEditPart findConnection(GraphicalEditPart node, ConnectionEditPart current, boolean forward) { List<ConnectionEditPart> connections = new ArrayList<ConnectionEditPart>(node.getSourceConnections()); connections.addAll(node.getTargetConnections()); if (connections.isEmpty()) return null; if (forward) counter++; else counter--; while (counter < 0) counter += connections.size(); counter %= connections.size(); return connections.get(counter % connections.size()); } /** * Given an absolute point (pStart) and a list of EditParts, this method finds the closest * EditPart (except for the one to be excluded) in the given direction. * * @param siblings List of sibling EditParts * @param pStart The starting point (must be in absolute coordinates) from which * the next sibling is to be found. * @param direction PositionConstants * @param exclude The EditPart to be excluded from the search * */ public IGraphicalEntityPart findSibling(List<?> siblings, Point pStart, int direction, EditPart exclude) { IGraphicalEntityPart epCurrent; IGraphicalEntityPart epFinal = null; IFigure figure; Point pCurrent; int distance = Integer.MAX_VALUE; Iterator<?> iter = siblings.iterator(); while (iter.hasNext()) { epCurrent = (IGraphicalEntityPart)iter.next(); if (epCurrent == exclude || !epCurrent.isSelectable()) continue; figure = epCurrent.getFigure(); pCurrent = getNavigationPoint(figure); figure.translateToAbsolute(pCurrent); if (pStart.getPosition(pCurrent) != direction) continue; Dimension difference = pCurrent.getDifference(pStart); int d = Math.abs(difference.width()) + Math.abs(difference.height()); if (d < distance) { distance = d; epFinal = epCurrent; } } return epFinal; } /** * Figures' navigation points are used to determine their direction compared to one * another, and the distance between them. * * @return the center of the given figure */ public Point getNavigationPoint(IFigure figure) { return figure.getBounds().getCenter(); } /** * This method is invoked when the user presses the space bar. It toggles the selection * of the EditPart that currently has focus. */ protected void processSelect(KeyEvent event) { EditPart part = getViewer().getFocusEditPart(); if ((event.stateMask & SWT.CONTROL) != 0 && part.getSelected() != EditPart.SELECTED_NONE) getViewer().deselect(part); else getViewer().appendSelection(part); getViewer().setFocus(part); } /** * @return <code>true</code> if key pressed indicates a connection traversal/selection */ boolean acceptConnection(KeyEvent event) { return event.character == '/' || event.character == '?' || event.character == '\\' || event.character == '\u001c' || event.character == '|'; } /** * This method navigates through connections based on the keys pressed. */ void navigateConnections(KeyEvent event) { GraphicalEditPart focus = (GraphicalEditPart) getFocusEntityPart(); ConnectionEditPart current = null; GraphicalEditPart node = getCachedNode(); if (focus instanceof ConnectionEditPart) { current = (ConnectionEditPart)focus; if (node == null || (node != current.getSource() && node != current.getTarget())) { node = (GraphicalEditPart)current.getSource(); counter = 0; } } else { node = focus; } setCachedNode(node); boolean forward = event.character == '/' || event.character == '?'; ConnectionEditPart next = findConnection(node, current, forward); navigateTo(next, event); } /** * Not yet implemented. */ boolean navigateJumpSibling(KeyEvent event, int direction) { // TODO: Implement navigateJumpSibling() (for PGUP, PGDN, HOME and END key events) return false; } public boolean isRootPart(IEntityPart focusPart) { return focusPart == getViewer().getContents(); } private EditPoint editPoint; public EditPoint getEditPoint() { if (editPoint == null) editPoint = new EditPoint((IEntityPart)getFocusEntityPart(), 0); else { IEntityPart focusPart = (IEntityPart)getFocusEntityPart(); if (editPoint.focus != focusPart) { editPoint.focus = focusPart; editPoint.caret = 0; } } return editPoint; } public boolean navigateModel(KeyEvent event, int direction) { EditPoint focusPoint = getEditPoint(); if (!(focusPoint.focus.getParent() instanceof IEntityPart)) return false; switch (direction) { case NORTH: IEntityPart parentPart = (IEntityPart)focusPoint.focus.getParent(); if (isRootPart(parentPart)) return false; editPoint.focus = parentPart; break; case SOUTH: List<?> children = focusPoint.focus.getChildren(); if (children.isEmpty()) return false; editPoint.focus = (IEntityPart)children.get(0); break; case EAST: parentPart = (IEntityPart)focusPoint.focus.getParent(); if (isRootPart(parentPart)) return false; List<?> siblings = parentPart.getChildren(); int index = siblings.indexOf(editPoint.focus); if (index < siblings.size()-1) editPoint.focus = findModelFirstChild((IEntityPart)siblings.get(index+1)); else editPoint.focus = parentPart; break; case WEST: parentPart = (IEntityPart)focusPoint.focus.getParent(); if (isRootPart(parentPart)) return false; siblings = parentPart.getChildren(); index = siblings.indexOf(editPoint.focus); if (index > 0) editPoint.focus = findModelLastChild((IEntityPart)siblings.get(index-1)); else editPoint.focus = parentPart; break; } editPoint.caret = 0; navigateTo(editPoint.focus, event); return true; } public IEntityPart findModelFirstChild(IEntityPart parent) { List<?> children = parent.getChildren(); if (children.isEmpty()) return parent; return findModelFirstChild((IEntityPart)children.get(0)); } public IEntityPart findModelLastChild(IEntityPart parent) { List<?> children = parent.getChildren(); if (children.isEmpty()) return parent; return findModelLastChild((IEntityPart)children.get(children.size()-1)); } public boolean navigateView(KeyEvent event, int direction) { EditPoint focusPoint = getEditPoint(); IGEFEditorKit editorKit = (IGEFEditorKit) focusPoint.focus.getModelEntity().wGetEditorKit(); IKeyHandler keyHandler = editorKit.getKeyHandler(); //FIXME workaround for a bug in navigation actions if (focusPoint.focus instanceof ITextualEntityPart) { ITextualEntityPart part = (ITextualEntityPart) focusPoint.focus; int start = part.getSelectionStart(); int end = part.getSelectionEnd(); if (start != -1 && end != -1) { CaretUpdater.sheduleSyncUpdate(part.getViewer(), part.getModelTextEntity(), direction == PositionConstants.WEST ? start : end, true); return true; } else { CaretUpdater.sheduleSyncUpdate(part.getViewer(), part.getModelTextEntity(), part.getCaretPosition(), true); } } editPoint = keyHandler.findNeighbour(this, focusPoint, direction); if (editPoint == null) return navigateNextSibling(event, direction); navigateTo(editPoint.focus, event); return true; } /** * Traverses to the next sibling in the given direction. * * @param event the KeyEvent for the keys that were pressed to trigger this traversal * @param direction PositionConstants.* indicating the direction in which to traverse */ boolean navigateNextSibling(KeyEvent event, int direction) { return navigateNextSibling(event, direction, getNavigationSiblings()); } /** * Traverses to the closest EditPart in the given list that is also in the given direction. * * @param event the KeyEvent for the keys that were pressed to trigger this traversal * @param direction PositionConstants.* indicating the direction in which to traverse */ boolean navigateNextSibling(KeyEvent event, int direction, List<?> list) { IEntityPart epStart = getFocusEntityPart(); if (epStart instanceof ITextualEntityPart) { ITextualEntityPart textualEntityPart = (ITextualEntityPart) epStart; ITextualFigure textualFigure = textualEntityPart.getTextualFigure(); String text = textualFigure.getText(); int line = CaretUtils.getLineFromPosition(text, textualEntityPart.getCaretPosition()); int lines = CaretUtils.getCaretLines(text); if ((direction == PositionConstants.WEST && textualEntityPart.getCaretPosition() > 0) || (direction == PositionConstants.EAST && textualEntityPart.getCaretPosition() < textualEntityPart.getCaretPositions())) { int position = textualEntityPart.getCaretPosition() + (direction == PositionConstants.WEST ? -1 : 1); CaretUpdater.sheduleSyncUpdate(getViewer(), textualEntityPart.getModelEntity(), position, true); return true; } else if ((direction == PositionConstants.NORTH && line > 0) || (direction == PositionConstants.SOUTH && line < lines)) { int dy = FigureUtilities.getFontMetrics(textualFigure.getFont()).getHeight() * (direction == PositionConstants.NORTH ? -1 : 1); Point location = textualFigure.getCaretBounds().getCenter().translate(0, dy); CaretUpdater.sheduleSyncUpdate(getViewer(), textualEntityPart.getModelEntity(), location, true); textualEntityPart.updateCaret(location); return true; } } if (!(epStart instanceof IGraphicalEntityPart)) return false; IFigure figure = ((IGraphicalEntityPart) epStart).getFigure(); Point pStart = getNavigationPoint(figure); figure.translateToAbsolute(pStart); EditPart next = findSibling(list, pStart, direction, epStart); // parent.findSibling(pStart, direction, epStart); while (next == null) { if (!(epStart.getParent() instanceof IGraphicalEntityPart)) return false; epStart = (IGraphicalEntityPart) epStart.getParent(); if (epStart == getViewer().getContents() || epStart.getParent() == getViewer().getContents() || epStart.getParent() == null) return false; list = epStart.getParent().getChildren(); next = findSibling(list, pStart, direction, epStart); } // next = next.enter(pStart, direction, epStart);1+2 navigateTo(next, event); return true; } /** * Navigates to the source or target of the currently focused ConnectionEditPart. */ void navigateOutOfConnection(KeyEvent event) { GraphicalEditPart cached = getCachedNode(); ConnectionEditPart conn = (ConnectionEditPart)getFocusEntityPart(); if (cached != null && (cached == conn.getSource() || cached == conn.getTarget())) navigateTo(cached, event); else navigateTo(conn.getSource(), event); } /** * Navigates to the given EditPart * * @param part the EditPart to navigate to * @param event the KeyEvent that triggered this traversal */ protected void navigateTo(EditPart part, KeyEvent event) { if (part == null) return; if ((event.stateMask & SWT.SHIFT) != 0) getViewer().appendSelection(part); else if (!isInsertMode()) getViewer().select(part); getViewer().setFocus(part); getViewer().reveal(part); } /** * @return <code>true</code> if the keys pressed indicate to stop traversing/selecting * connection */ boolean acceptLeaveConnection(KeyEvent event) { int key = event.keyCode; if (getFocusEntityPart() instanceof ConnectionEditPart) if ((key == SWT.ARROW_UP) || (key == SWT.ARROW_RIGHT) || (key == SWT.ARROW_DOWN) || (key == SWT.ARROW_LEFT)) return true; return false; } public boolean keyPressed(KeyEvent event) { //FIXME workaround (whent textual tool is enabled // inhibit navigation actions that use printable chars) if (event.keyCode == SWT.INSERT) { toggleInsertMode(); return true; } else if (event.character == ' ' && !Tools.TEXTUAL.isActive(getViewer())) { processSelect(event); return true; } else if (acceptConnection(event) && !Tools.TEXTUAL.isActive(getViewer())) { navigateConnections(event); return true; } else if (acceptLeaveConnection(event) && !Tools.TEXTUAL.isActive(getViewer())) { navigateOutOfConnection(event); return true; } if ((event.stateMask & SWT.CTRL) != 0) { switch (event.keyCode) { case SWT.ARROW_LEFT: if (navigateModel(event, PositionConstants.WEST)) return true; break; case SWT.ARROW_RIGHT: if (navigateModel(event, PositionConstants.EAST)) return true; break; case SWT.ARROW_UP: if (navigateModel(event, PositionConstants.NORTH)) return true; break; case SWT.ARROW_DOWN: if (navigateModel(event, PositionConstants.SOUTH)) return true; break; } } switch (event.keyCode) { case SWT.ARROW_LEFT: if (navigateView(event, PositionConstants.WEST)) return true; break; case SWT.ARROW_RIGHT: if (navigateView(event, PositionConstants.EAST)) return true; break; case SWT.ARROW_UP: if (navigateView(event, PositionConstants.NORTH)) return true; break; case SWT.ARROW_DOWN: if (navigateView(event, PositionConstants.SOUTH)) return true; break; case SWT.HOME: if (navigateJumpSibling(event, PositionConstants.WEST)) return true; break; case SWT.END: if (navigateJumpSibling(event, PositionConstants.EAST)) return true; break; case SWT.PAGE_DOWN: if (navigateJumpSibling(event, PositionConstants.SOUTH)) return true; break; case SWT.PAGE_UP: if (navigateJumpSibling(event, PositionConstants.NORTH)) return true; } return super.keyPressed(event); } }