| /******************************************************************************* |
| * Copyright (c) 2000, 2010 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.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.draw2d.rap.swt.SWT; |
| |
| 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 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 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()); |
| 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.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 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 = new WeakReference(node); |
| } |
| |
| } |