/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.perflib.vmtrace.viz; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; /** * {@link ZoomPanInteractor} listens to mouse events, interprets dragging the mouse as an attempt * to pan the canvas, and mouse wheel rotation as an attempt to zoom in/out the canvas. * After such events, it updates its listeners with the updated transformation matrix corresponding * to the zoom & pan values. */ public class ZoomPanInteractor implements MouseListener, MouseMotionListener, MouseWheelListener { /** * The values from {@link java.awt.event.MouseWheelEvent#getWheelRotation()} are quite high even * for a small amount of scrolling. This is an arbitrary scale factor used to go from the wheel * rotation value to a zoom by factor. The scale is negated to take care of the common * expectation that scrolling down should zoom out, not zoom in. */ private static final double WHEEL_UNIT_SCALE = -0.1; private final AffineTransform mTransform = new AffineTransform(); private AffineTransform mInverseTransform; private final Point2D mTmpPoint = new Point2D.Double(); private int mLastX; private int mLastY; private final List<ViewTransformListener> mListeners = new ArrayList<ViewTransformListener>(); @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { mLastX = e.getX(); mLastY = e.getY(); } @Override public void mouseDragged(MouseEvent e) { int deltaX = e.getX() - mLastX; int deltaY = e.getY() - mLastY; translateBy(deltaX, deltaY); notifyTransformChange(); mLastX = e.getX(); mLastY = e.getY(); } @VisibleForTesting void translateBy(int deltaX, int deltaY) { // Transform pixels by the current viewport scaling factor. // i.e when you have zoomed out by say 2x, and you drag by a pixel, // you expect it to move (the model space) by 2 pixels, not one. deltaX /= mTransform.getScaleX(); deltaY /= mTransform.getScaleY(); mTransform.translate(deltaX, deltaY); // Do not allow panning above the axis. // TODO: This actually encodes information about the canvas and what is drawn over here, // and should be moved out. if (mTransform.getTranslateY() > 0) { mTransform.translate(0, -mTransform.getTranslateY()); } } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (e.getScrollType() != MouseWheelEvent.WHEEL_UNIT_SCROLL) { return; } double scale = 1 + WHEEL_UNIT_SCALE * e.getWheelRotation(); // convert mouse x, y from screen coordinates to absolute coordinates mTmpPoint.setLocation(e.getX(), e.getY()); mInverseTransform.transform(mTmpPoint, mTmpPoint); zoomBy(scale, 1, mTmpPoint); notifyTransformChange(); } @VisibleForTesting void zoomBy(double scaleX, double scaleY, Point2D location) { // When zooming, we want to zoom by the location the mouse currently points to. // So we translate the current location to the origin, apply the scale, and translate back mTransform.translate(location.getX(), location.getY()); mTransform.scale(scaleX, scaleY); mTransform.translate(-location.getX(), -location.getY()); } private void notifyTransformChange() { try { mInverseTransform = mTransform.createInverse(); } catch (NoninvertibleTransformException ignored) { // The transform matrix is only scaled or translated, both of which are invertible. } for (ViewTransformListener l : mListeners) { l.transformChanged(mTransform); } } public void setToScaleX(double sx, double sy) { mTransform.setToScale(sx, sy); notifyTransformChange(); } @VisibleForTesting AffineTransform getTransform() { return mTransform; } public interface ViewTransformListener { void transformChanged(@NonNull AffineTransform transform); } public void addViewTransformListener(@NonNull ViewTransformListener l) { mListeners.add(l); } }