/******************************************************************************* * Copyright (c) 2000, 2007 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.ui.parts; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; 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; /** * 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 CONTROL modifiers * <LI>Arrow Keys (UP, DOWN) same as above, but with ALT modifier. * <LI>'\'Backslash and '/' Slash keys with optional SHIFT and CONTROL modifiers * </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 WeakReference 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()); 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()); } /** * 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 = siblings.iterator(); while (iter.hasNext()) { epCurrent = (GraphicalEditPart)iter.next(); if (epCurrent == exclude || !epCurrent.isSelectable()) 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.get(); } /** * @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 (event.character == ' ') { 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); } /** * 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 = 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 ged = (GraphicalEditPart)childList.get(i); if (!ged.isSelectable()) continue; Rectangle childBounds = ged.getFigure().getBounds(); current = (childBounds.x - tl.x) + (childBounds.y - tl.y); if (current < minimum) { minimum = current; closestPart = ged; } } 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() || getFocusEditPart().getParent() == getViewer().getContents()) return; navigateTo(getFocusEditPart().getParent(), 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.CONTROL) != 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 = new WeakReference(node); } }