/******************************************************************************* * Copyright (c) 2000, 2010, 2012 IBM Corporation, Gerhardt Informatics Kft. 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 * Gerhardt Informatics Kft. - GEFGWT port *******************************************************************************/ package org.eclipse.gef.ui.parts; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.draw2d.FigureCanvas; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.PositionConstants; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.gef.ConnectionEditPart; import org.eclipse.gef.EditPart; import org.eclipse.gef.GraphicalEditPart; import org.eclipse.gef.GraphicalViewer; import org.eclipse.gef.KeyHandler; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; /** * An extended KeyHandler which processes default keystrokes for common * navigation in a GraphicalViewer. This class can be used as a KeyHandler too; * Unrecognized keystrokes are sent to the super's implementation. This class * will process key events containing the following: * <UL> * <LI>Arrow Keys (UP, DOWN, LEFT, RIGHT) with optional SHIFT and CTRL modifiers * to navigate between siblings. * <LI>Arrow Keys (UP, DOWN) same as above, but with ALT modifier to navigate * into or out of a container. * <LI>'\'Backslash and '/' Slash keys with optional SHIFT and CTRL modifiers to * traverse connections. * </UL> * <P> * All processed key events will do nothing other than change the selection * and/or focus editpart for the viewer. * * @author hudsonr */ public class GraphicalViewerKeyHandler extends KeyHandler { int counter; // /** // * When navigating through connections, a "Node" EditPart is used as a // * reference. // */ private Object cachedNode; private GraphicalViewer viewer; /** * Constructs a key handler for the given viewer. * * @param viewer * the viewer */ public GraphicalViewerKeyHandler(GraphicalViewer viewer) { this.viewer = viewer; } /** * @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 == '|'; } /** * @return <code>true</code> if the keys pressed indicate to traverse inside * a container */ boolean acceptIntoContainer(KeyEvent event) { return ((event.stateMask & SWT.ALT) != 0) && (event.keyCode == SWT.ARROW_DOWN); } /** * @return <code>true</code> if the keys pressed indicate to stop * traversing/selecting connection */ boolean acceptLeaveConnection(KeyEvent event) { int key = event.keyCode; if (getFocusEditPart() instanceof ConnectionEditPart) if ((key == SWT.ARROW_UP) || (key == SWT.ARROW_RIGHT) || (key == SWT.ARROW_DOWN) || (key == SWT.ARROW_LEFT)) return true; return false; } /** * @return <code>true</code> if the viewer's contents has focus and one of * the arrow keys is pressed */ boolean acceptLeaveContents(KeyEvent event) { int key = event.keyCode; return getFocusEditPart() == getViewer().getContents() && ((key == SWT.ARROW_UP) || (key == SWT.ARROW_RIGHT) || (key == SWT.ARROW_DOWN) || (key == SWT.ARROW_LEFT)); } /** * @return <code>true</code> if the keys pressed indicate to traverse to the * parent of the currently focused EditPart */ boolean acceptOutOf(KeyEvent event) { return ((event.stateMask & SWT.ALT) != 0) && (event.keyCode == SWT.ARROW_UP); } boolean acceptScroll(KeyEvent event) { return ((event.stateMask & SWT.CTRL) != 0 && (event.stateMask & SWT.SHIFT) != 0 && (event.keyCode == SWT.ARROW_DOWN || event.keyCode == SWT.ARROW_LEFT || event.keyCode == SWT.ARROW_RIGHT || event.keyCode == SWT.ARROW_UP)); } /** * 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 */ ConnectionEditPart findConnection(GraphicalEditPart node, ConnectionEditPart current, boolean forward) { List connections = new ArrayList(node.getSourceConnections()); connections.addAll(node.getTargetConnections()); connections = getValidNavigationTargets(connections); if (connections.isEmpty()) return null; if (forward) counter++; else counter--; while (counter < 0) counter += connections.size(); counter %= connections.size(); return (ConnectionEditPart) connections.get(counter % connections.size()); } private List getValidNavigationTargets(List candidateEditParts) { List validNavigationTargetEditParts = new ArrayList(); for (int i = 0; i < candidateEditParts.size(); i++) { EditPart candidate = (EditPart) candidateEditParts.get(i); if (isValidNavigationTarget(candidate)) { validNavigationTargetEditParts.add(candidate); } } return validNavigationTargetEditParts; } /** * 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 * */ GraphicalEditPart findSibling(List siblings, Point pStart, int direction, EditPart exclude) { GraphicalEditPart epCurrent; GraphicalEditPart epFinal = null; IFigure figure; Point pCurrent; int distance = Integer.MAX_VALUE; Iterator iter = getValidNavigationTargets(siblings).iterator(); while (iter.hasNext()) { epCurrent = (GraphicalEditPart) iter.next(); if (epCurrent == exclude) continue; figure = epCurrent.getFigure(); pCurrent = getNavigationPoint(figure); figure.translateToAbsolute(pCurrent); if (pStart.getPosition(pCurrent) != direction) continue; int d = pCurrent.getDistanceOrthogonal(pStart); 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 */ Point getNavigationPoint(IFigure figure) { return figure.getBounds().getCenter(); } /** * Returns the cached node. It is possible that the node is not longer in * the viewer but has not been garbage collected yet. */ private GraphicalEditPart getCachedNode() { if (cachedNode == null) return null; // if (cachedNode.isEnqueued()) // return null; return (GraphicalEditPart) cachedNode; } /** * @return the EditPart that has focus */ protected GraphicalEditPart getFocusEditPart() { return (GraphicalEditPart) getViewer().getFocusEditPart(); } /** * Returns the list of editparts which are conceptually at the same level of * navigation as the currently focused editpart. By default, these are the * siblings of the focused part. * <p> * This implementation returns a list that contains the EditPart that has * focus. * </p> * * @return a list of navigation editparts * @since 3.4 */ protected List getNavigationSiblings() { EditPart focusPart = getFocusEditPart(); if (focusPart.getParent() != null) return focusPart.getParent().getChildren(); List list = new ArrayList(); list.add(focusPart); return list; } /** * Returns the viewer on which this key handler was created. * * @return the viewer */ protected GraphicalViewer getViewer() { return viewer; } /** * @return <code>true</code> if the viewer is mirrored * @since 3.4 */ protected boolean isViewerMirrored() { return (viewer.getControl().getStyle() & SWT.MIRRORED) != 0; } /** * Extended to process key events described above. * * @see org.eclipse.gef.KeyHandler#keyPressed(org.eclipse.swt.events.KeyEvent) */ public boolean keyPressed(KeyEvent event) { // if CTRL + SPACE is pressed, event.character == ' ' does not hold; // therefore using the keyCode to decide whether SPACE was pressed (with // or without modifiers). if (event.keyCode == 32) { processSelect(event); return true; } else if (acceptIntoContainer(event)) { navigateIntoContainer(event); return true; } else if (acceptOutOf(event)) { navigateOut(event); return true; } else if (acceptConnection(event)) { navigateConnections(event); return true; } else if (acceptScroll(event)) { scrollViewer(event); return true; } else if (acceptLeaveConnection(event)) { navigateOutOfConnection(event); return true; } else if (acceptLeaveContents(event)) { navigateIntoContainer(event); return true; } switch (event.keyCode) { case SWT.ARROW_LEFT: if (navigateNextSibling(event, isViewerMirrored() ? PositionConstants.EAST : PositionConstants.WEST)) return true; break; case SWT.ARROW_RIGHT: if (navigateNextSibling(event, isViewerMirrored() ? PositionConstants.WEST : PositionConstants.EAST)) return true; break; case SWT.ARROW_UP: if (navigateNextSibling(event, PositionConstants.NORTH)) return true; break; case SWT.ARROW_DOWN: if (navigateNextSibling(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); } /** * This method navigates through connections based on the keys pressed. */ void navigateConnections(KeyEvent event) { GraphicalEditPart focus = getFocusEditPart(); 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); } private boolean isValidNavigationTarget(EditPart editPart) { return editPart.isSelectable(); } /** * This method traverses to the closest child of the currently focused * EditPart, if it has one. */ void navigateIntoContainer(KeyEvent event) { GraphicalEditPart focus = getFocusEditPart(); List childList = getValidNavigationTargets(focus.getChildren()); Point tl = focus.getContentPane().getBounds().getTopLeft(); int minimum = Integer.MAX_VALUE; int current; GraphicalEditPart closestPart = null; for (int i = 0; i < childList.size(); i++) { GraphicalEditPart child = (GraphicalEditPart) childList.get(i); Rectangle childBounds = child.getFigure().getBounds(); current = (childBounds.x - tl.x) + (childBounds.y - tl.y); if (current < minimum) { minimum = current; closestPart = child; } } if (closestPart != null) navigateTo(closestPart, event); } /** * Not yet implemented. */ boolean navigateJumpSibling(KeyEvent event, int direction) { // TODO: Implement navigateJumpSibling() (for PGUP, PGDN, HOME and END // key events) return false; } /** * 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) { GraphicalEditPart epStart = getFocusEditPart(); IFigure figure = epStart.getFigure(); Point pStart = getNavigationPoint(figure); figure.translateToAbsolute(pStart); EditPart next = findSibling(list, pStart, direction, epStart); if (next == null) return false; navigateTo(next, event); return true; } /** * Navigates to the parent of the currently focused EditPart. */ void navigateOut(KeyEvent event) { if (getFocusEditPart() == null || getFocusEditPart() == getViewer().getContents()) return; EditPart editPart = getFocusEditPart().getParent(); while (editPart != null && editPart != getViewer().getContents() && !isValidNavigationTarget(editPart)) { editPart = editPart.getParent(); } if (editPart != null && isValidNavigationTarget(editPart)) { navigateTo(editPart, event); } } /** * Navigates to the source or target of the currently focused * ConnectionEditPart. */ void navigateOutOfConnection(KeyEvent event) { GraphicalEditPart cached = getCachedNode(); ConnectionEditPart conn = (ConnectionEditPart) getFocusEditPart(); 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); getViewer().setFocus(part); } else if ((event.stateMask & SWT.CONTROL) != 0) getViewer().setFocus(part); else getViewer().select(part); getViewer().reveal(part); } /** * This method is invoked when the user presses the space bar. It toggles * the selection of the EditPart that currently has focus. * * @param event * the key event received */ protected void processSelect(KeyEvent event) { EditPart part = getViewer().getFocusEditPart(); if (part != getViewer().getContents()) { if ((event.stateMask & SWT.CTRL) != 0 && part.getSelected() != EditPart.SELECTED_NONE) getViewer().deselect(part); else getViewer().appendSelection(part); getViewer().setFocus(part); } } void scrollViewer(KeyEvent event) { if (!(getViewer().getControl() instanceof FigureCanvas)) return; FigureCanvas figCanvas = (FigureCanvas) getViewer().getControl(); Point loc = figCanvas.getViewport().getViewLocation(); Rectangle area = figCanvas.getViewport() .getClientArea(Rectangle.SINGLETON).scale(.1); switch (event.keyCode) { case SWT.ARROW_DOWN: figCanvas.scrollToY(loc.y + area.height); break; case SWT.ARROW_UP: figCanvas.scrollToY(loc.y - area.height); break; case SWT.ARROW_LEFT: if (isViewerMirrored()) figCanvas.scrollToX(loc.x + area.width); else figCanvas.scrollToX(loc.x - area.width); break; case SWT.ARROW_RIGHT: if (isViewerMirrored()) figCanvas.scrollToX(loc.x - area.width); else figCanvas.scrollToX(loc.x + area.width); } } private void setCachedNode(GraphicalEditPart node) { if (node == null) cachedNode = null; else cachedNode = node; } }