package org.archstudio.bna.logics.navigating; import static org.archstudio.sysutils.SystemUtils.castOrNull; import java.util.List; import java.util.Set; import org.archstudio.bna.IBNAView; import org.archstudio.bna.IBNAWorld; import org.archstudio.bna.ICoordinate; import org.archstudio.bna.IMutableCoordinateMapper; import org.archstudio.bna.IThing; import org.archstudio.bna.constants.GestureType; import org.archstudio.bna.constants.KeyType; import org.archstudio.bna.constants.MouseType; import org.archstudio.bna.logics.AbstractThingLogic; import org.archstudio.bna.logics.tracking.ModelBoundsTrackingLogic; import org.archstudio.bna.ui.IBNAAllEventsListener2; import org.archstudio.bna.ui.IBNAKeyListener2; import org.archstudio.bna.ui.IBNAMagnifyGestureListener; import org.archstudio.bna.ui.IBNAMouseClickListener2; import org.archstudio.bna.ui.IBNAMouseMoveListener2; import org.archstudio.bna.ui.IBNAMouseWheelListener2; import org.archstudio.bna.ui.IBNAPanGestureListener; import org.archstudio.bna.ui.IBNATrackGestureListener; import org.archstudio.bna.utils.BNAUtils; import org.archstudio.bna.utils.BNAUtils2.ThingsAtLocation; import org.archstudio.bna.utils.DefaultCoordinate; import org.archstudio.sysutils.SystemUtils; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.GestureEvent; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import com.google.common.collect.Sets; public class PanAndZoomLogic extends AbstractThingLogic implements IBNAMouseClickListener2, IBNAMouseMoveListener2, IBNAMouseWheelListener2, IBNAAllEventsListener2, IBNATrackGestureListener, IBNAMagnifyGestureListener, IBNAPanGestureListener, IBNAKeyListener2 { protected static final int PAN_BUTTON = 2; // middle button protected static boolean inGesture = false; protected static double originalScale = 1; protected static double minScale = Math.pow(2, -10); protected static double maxScale = Math.pow(2, 10); protected static Set<Composite> registeredComposites = Sets.newHashSet(); protected static Listener preventScrollListener = new Listener() { @Override public void handleEvent(Event event) { if (!inGesture) { event.doit = false; } } }; protected final ModelBoundsTrackingLogic boundsLogic; protected ICoordinate startMouseCoordinate = null; public PanAndZoomLogic(IBNAWorld world) { super(world); this.boundsLogic = logics.addThingLogic(ModelBoundsTrackingLogic.class); } protected static void registerView(IBNAView view) { final Composite composite = view.getBNAUI().getComposite(); if (registeredComposites.add(composite)) { composite.addListener(SWT.MouseVerticalWheel, preventScrollListener); composite.addListener(SWT.MouseHorizontalWheel, preventScrollListener); composite.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { registeredComposites.remove(composite); } }); } } @Override public void mouseDown(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } if (evt.button == PAN_BUTTON) { startMouseCoordinate = DefaultCoordinate.forLocal(new Point(evt.x, evt.y), view.getCoordinateMapper()); Composite composite = view.getBNAUI().getComposite(); composite.setCursor(composite.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); } } @Override public void mouseUp(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } if (startMouseCoordinate != null) { startMouseCoordinate = null; view.getBNAUI().getComposite().setCursor(null); } } @Override public void mouseClick(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { } @Override public void mouseMove(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } registerView(view); if (startMouseCoordinate != null) { IMutableCoordinateMapper mcm = castOrNull(view.getCoordinateMapper(), IMutableCoordinateMapper.class); if (mcm != null) { mcm.align(new Point(evt.x, evt.y), startMouseCoordinate.getWorldPoint()); } } } @Override public void mouseHorizontalWheel(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { } @Override public void mouseVerticalWheel(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location, ThingsAtLocation thingsAtLocation) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } registerView(view); // only handle events for the top world if (view.getParentView() != null) { return; } if (!inGesture) { zoom(view, DefaultCoordinate.forLocal(new Point(evt.x, evt.y), view.getCoordinateMapper()), evt.count < 0 ? -1 : 1); } } @Override public void magnifyGesture(IBNAView view, GestureType type, GestureEvent e, List<IThing> t, ICoordinate location) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } IMutableCoordinateMapper mcm = castOrNull(view.getCoordinateMapper(), IMutableCoordinateMapper.class); if (mcm != null) { mcm.setLocalScaleAndAlign(SystemUtils.bound(minScale, originalScale * e.magnification, maxScale), location.getLocalPoint(), location.getWorldPoint()); } } @Override public void beginGesture(IBNAView view, GestureType type, GestureEvent e, List<IThing> t, ICoordinate location) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } inGesture = true; originalScale = view.getCoordinateMapper().getLocalScale(); } @Override public void endGesture(IBNAView view, GestureType type, GestureEvent e, List<IThing> t, ICoordinate location) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } inGesture = false; } @Override public void panGesture(IBNAView view, GestureType type, GestureEvent e, List<IThing> t, ICoordinate location) { // This seems to happen automatically using the scrollbars } @Override public void keyPressed(IBNAView view, KeyType type, KeyEvent e) { BNAUtils.checkLock(); // only handle events for the top world if (view.getParentView() != null) { return; } if ((e.stateMask & SWT.MOD1) != 0) { int delta = 0; switch (e.character) { case '-': delta = -1; break; case '+': case '=': delta = 1; break; } if (delta != 0) { Rectangle bounds = view.getBNAUI().getComposite().getBounds(); Point center = new Point(bounds.width / 2, bounds.height / 2); ICoordinate location = DefaultCoordinate.forLocal(center, view.getCoordinateMapper()); zoom(view, location, delta); } } } @Override public void keyReleased(IBNAView view, KeyType type, KeyEvent e) { } private void zoom(IBNAView view, ICoordinate location, int delta) { IMutableCoordinateMapper mcm = castOrNull(view.getCoordinateMapper(), IMutableCoordinateMapper.class); if (mcm != null) { double oldScale = mcm.getLocalScale(); double oldPower = Math.log(oldScale) / Math.log(Math.sqrt(2)); double newPower = oldPower + delta; double newScale = Math.pow(Math.sqrt(2), newPower); mcm.setLocalScaleAndAlign(SystemUtils.bound(minScale, newScale, maxScale), location.getLocalPoint(), location.getWorldPoint()); } } }