/**
* Copyright (C) 2012 Iordan Iordanov
* Copyright (C) 2009 Michael A. MacDonald
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
package com.iiordanov.bVNC.input;
import java.util.LinkedList;
import java.util.Queue;
import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import com.iiordanov.android.bc.BCFactory;
import com.iiordanov.android.bc.IBCScaleGestureDetector;
import com.iiordanov.android.bc.OnScaleGestureListener;
import com.iiordanov.bVNC.Constants;
import com.iiordanov.bVNC.RemoteCanvas;
import com.iiordanov.bVNC.RemoteCanvasActivity;
/**
* An AbstractInputHandler that uses GestureDetector to detect standard gestures in touch events
*
* @author Michael A. MacDonald
*/
abstract class AbstractGestureInputHandler extends GestureDetector.SimpleOnGestureListener
implements AbstractInputHandler, OnScaleGestureListener {
private static final String TAG = "AbstractGestureInputHandler";
protected GestureDetector gestures;
protected IBCScaleGestureDetector scaleGestures;
/**
* Handles to the VncCanvas view and RemoteCanvasActivity activity.
*/
protected RemoteCanvas canvas;
protected RemoteCanvasActivity activity;
/**
* Key handler delegate that handles DPad-based mouse motion
*/
protected DPadMouseKeyHandler keyHandler;
// This is the initial "focal point" of the gesture (between the two fingers).
float xInitialFocus;
float yInitialFocus;
// This is the final "focal point" of the gesture (between the two fingers).
float xCurrentFocus;
float yCurrentFocus;
float xPreviousFocus;
float yPreviousFocus;
// These variables record whether there was a two-finger swipe performed up or down.
boolean inSwiping = false;
boolean twoFingerSwipeUp = false;
boolean twoFingerSwipeDown = false;
boolean twoFingerSwipeLeft = false;
boolean twoFingerSwipeRight = false;
// These variables indicate whether the dpad should be used as arrow keys
// and whether it should be rotated.
boolean useDpadAsArrows = false;
boolean rotateDpad = false;
boolean trackballButtonDown;
// The variables which indicates how many scroll events to send per swipe
// event and the maximum number to send at one time.
long swipeSpeed = 1;
int maxSwipeSpeed = 20;
// If swipe events are registered once every baseSwipeTime miliseconds, then
// swipeSpeed will be one. If more often, swipe-speed goes up, if less, down.
final long baseSwipeTime = 600;
// This is how far the swipe has to travel before a swipe event is generated.
final float baseSwipeDist = 40.f;
// This is how far from the top and bottom edge to detect immersive swipe.
final float immersiveSwipeDistance = 50.f;
boolean immersiveSwipe = false;
boolean inScrolling = false;
boolean inScaling = false;
boolean scalingJustFinished = false;
// The minimum distance a scale event has to traverse the FIRST time before scaling starts.
final double minScaleFactor = 0.1;
// What action was previously performed by a mouse or stylus.
int prevMouseOrStylusAction = 0;
// What the display density is.
float displayDensity = 0;
// Indicates that the next onFling will be disregarded.
boolean disregardNextOnFling = false;
// Queue which holds the last two MotionEvents which triggered onScroll
Queue<Float> distXQueue;
Queue<Float> distYQueue;
/**
* In the drag modes, we process mouse events without sending them through
* the gesture detector.
*/
protected boolean panMode = false;
protected boolean dragMode = false;
protected boolean rightDragMode = false;
protected boolean middleDragMode = false;
protected float dragX, dragY;
protected boolean singleHandedGesture = false;
protected boolean singleHandedJustEnded = false;
/**
* These variables keep track of which pointers have seen ACTION_DOWN events.
*/
protected boolean secondPointerWasDown = false;
protected boolean thirdPointerWasDown = false;
AbstractGestureInputHandler(RemoteCanvasActivity c, RemoteCanvas v, boolean slowScrolling)
{
activity = c;
canvas = v;
gestures=BCFactory.getInstance().getBCGestureDetector().createGestureDetector(c, this);
gestures.setOnDoubleTapListener(this);
scaleGestures=BCFactory.getInstance().getScaleGestureDetector(c, this);
useDpadAsArrows = activity.getUseDpadAsArrows();
rotateDpad = activity.getRotateDpad();
keyHandler = new DPadMouseKeyHandler(activity, canvas.handler, useDpadAsArrows, rotateDpad);
displayDensity = canvas.getDisplayDensity();
distXQueue = new LinkedList<Float>();
distYQueue = new LinkedList<Float>();
if (slowScrolling) {
maxSwipeSpeed = 2;
}
}
/**
* Function to get appropriate X coordinate from motion event for this input handler.
* @return the appropriate X coordinate.
*/
protected int getX (MotionEvent e) {
float scale = canvas.getScale();
return (int)(canvas.getAbsoluteX() + e.getX() / scale);
}
/**
* Function to get appropriate Y coordinate from motion event for this input handler.
* @return the appropriate Y coordinate.
*/
protected int getY (MotionEvent e) {
float scale = canvas.getScale();
return (int)(canvas.getAbsoluteY() + (e.getY() - 1.f * canvas.getTop()) / scale);
}
/**
* Handles actions performed by a mouse.
* @param e touch or generic motion event
* @return
*/
protected boolean handleMouseActions (MotionEvent e) {
final int action = e.getActionMasked();
final int meta = e.getMetaState();
final int bstate = e.getButtonState();
RemotePointer p = canvas.getPointer();
float scale = canvas.getScale();
int x = (int)(canvas.getAbsoluteX() + e.getX() / scale);
int y = (int)(canvas.getAbsoluteY() + (e.getY() - 1.f * canvas.getTop()) / scale);
switch (action) {
// If a mouse button was pressed or mouse was moved.
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
switch (bstate) {
case MotionEvent.BUTTON_PRIMARY:
canvas.panToMouse();
return p.processPointerEvent(x, y, action, meta, true, false, false, false, 0);
case MotionEvent.BUTTON_SECONDARY:
canvas.panToMouse();
return p.processPointerEvent(x, y, action, meta, true, true, false, false, 0);
case MotionEvent.BUTTON_TERTIARY:
canvas.panToMouse();
return p.processPointerEvent(x, y, action, meta, true, false, true, false, 0);
}
break;
// If a mouse button was released.
case MotionEvent.ACTION_UP:
switch (bstate) {
case 0:
if (e.getToolType(0) != MotionEvent.TOOL_TYPE_MOUSE) {
break;
}
case MotionEvent.BUTTON_PRIMARY:
case MotionEvent.BUTTON_SECONDARY:
case MotionEvent.BUTTON_TERTIARY:
canvas.panToMouse();
return p.processPointerEvent(x, y, action, meta, false, false, false, false, 0);
}
break;
// If the mouse wheel was scrolled.
case MotionEvent.ACTION_SCROLL:
float vscroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL);
float hscroll = e.getAxisValue(MotionEvent.AXIS_HSCROLL);
int swipeSpeed = 0, direction = 0;
if (vscroll < 0) {
swipeSpeed = (int)(-1*vscroll);
direction = 1;
} else if (vscroll > 0) {
swipeSpeed = (int)vscroll;
direction = 0;
} else if (hscroll < 0) {
swipeSpeed = (int)(-1*hscroll);
direction = 3;
} else if (hscroll > 0) {
swipeSpeed = (int)hscroll;
direction = 2;
} else
return false;
int numEvents = 0;
while (numEvents < swipeSpeed) {
p.processPointerEvent(x, y, action, meta, true, false, false, true, direction);
p.processPointerEvent(x, y, action, meta, false, false, false, false, 0);
numEvents++;
}
break;
// If the mouse was moved OR as reported, some external mice trigger this when a
// mouse button is pressed as well, so we check bstate here too.
case MotionEvent.ACTION_HOVER_MOVE:
canvas.panToMouse();
switch (bstate) {
case MotionEvent.BUTTON_PRIMARY:
return p.processPointerEvent(x, y, action, meta, true, false, false, false, 0);
case MotionEvent.BUTTON_SECONDARY:
return p.processPointerEvent(x, y, action, meta, true, true, false, false, 0);
case MotionEvent.BUTTON_TERTIARY:
return p.processPointerEvent(x, y, action, meta, true, false, true, false, 0);
default:
return p.processPointerEvent(x, y, action, meta, false, false, false, false, 0);
}
}
prevMouseOrStylusAction = action;
return false;
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(android.view.MotionEvent)
*/
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
RemotePointer p = canvas.getPointer();
final int action = e.getActionMasked();
final int meta = e.getMetaState();
activity.showZoomer(true);
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, false, 0);
SystemClock.sleep(50);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
canvas.panToMouse();
return true;
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onDoubleTap(android.view.MotionEvent)
*/
@Override
public boolean onDoubleTap (MotionEvent e) {
RemotePointer p = canvas.getPointer();
final int action = e.getActionMasked();
final int meta = e.getMetaState();
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, false, 0);
SystemClock.sleep(50);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
SystemClock.sleep(50);
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, false, 0);
SystemClock.sleep(50);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
canvas.panToMouse();
return true;
}
/*
* (non-Javadoc)
*
* @see android.view.GestureDetector.SimpleOnGestureListener#onLongPress(android.view.MotionEvent)
*/
@Override
public void onLongPress(MotionEvent e) {
RemotePointer p = canvas.getPointer();
// If we've performed a right/middle-click and the gesture is not over yet, do not start drag mode.
if (secondPointerWasDown || thirdPointerWasDown)
return;
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
dragMode = true;
p.processPointerEvent(getX(e), getY(e), e.getActionMasked(), e.getMetaState(), true, false, false, false, 0);
}
protected boolean endDragModesAndScrolling () {
canvas.inScrolling = false;
panMode = false;
inScaling = false;
inSwiping = false;
inScrolling = false;
immersiveSwipe = false;
if (dragMode || rightDragMode || middleDragMode) {
dragMode = false;
rightDragMode = false;
middleDragMode = false;
return true;
} else {
return false;
}
}
private void detectImmersiveSwipe (float y) {
if (Constants.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
(y <= immersiveSwipeDistance || canvas.getHeight() - y <= immersiveSwipeDistance)) {
inSwiping = true;
immersiveSwipe = true;
} else {
inSwiping = false;
immersiveSwipe = false;
}
}
/**
* Modify the event so that the mouse goes where we specify.
* @param e event to be modified.
* @param x new x coordinate.
* @param y new y coordinate.
*/
private void setEventCoordinates(MotionEvent e, float x, float y) {
e.setLocation(x, y);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
final int action = e.getActionMasked();
final int index = e.getActionIndex();
final int pointerID = e.getPointerId(index);
final int meta = e.getMetaState();
RemotePointer p = canvas.getPointer();
float f = e.getPressure();
if (f > 2.f)
f = f / 50.f;
if (f > .92f) {
disregardNextOnFling = true;
}
if (android.os.Build.VERSION.SDK_INT >= 14) {
// Handle and consume actions performed by a (e.g. USB or bluetooth) mouse.
if (handleMouseActions (e))
return true;
}
if (action == MotionEvent.ACTION_UP) {
// Turn filtering back on and invalidate to make things pretty.
canvas.bitmapData.drawable._defaultPaint.setFilterBitmap(true);
canvas.invalidate();
}
switch (pointerID) {
case 0:
switch (action) {
case MotionEvent.ACTION_DOWN:
disregardNextOnFling = false;
singleHandedJustEnded = false;
// We have put down first pointer on the screen, so we can reset the state of all click-state variables.
// Permit sending mouse-down event on long-tap again.
secondPointerWasDown = false;
// Permit right-clicking again.
thirdPointerWasDown = false;
// Cancel any effect of scaling having "just finished" (e.g. ignoring scrolling).
scalingJustFinished = false;
// Cancel drag modes and scrolling.
if (!singleHandedGesture)
endDragModesAndScrolling();
canvas.inScrolling = true;
// If we are manipulating the desktop, turn off bitmap filtering for faster response.
canvas.bitmapData.drawable._defaultPaint.setFilterBitmap(false);
dragX = e.getX();
dragY = e.getY();
// Detect whether this is potentially the start of a gesture to show the nav bar.
detectImmersiveSwipe(dragY);
break;
case MotionEvent.ACTION_UP:
singleHandedGesture = false;
singleHandedJustEnded = true;
// If this is the end of a swipe that showed the nav bar, consume.
if (immersiveSwipe && Math.abs(dragY - e.getY()) > immersiveSwipeDistance) {
endDragModesAndScrolling();
return true;
}
// If any drag modes were going on, end them and send a mouse up event.
if (endDragModesAndScrolling())
return p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
break;
case MotionEvent.ACTION_MOVE:
// Send scroll up/down events if swiping is happening.
if (panMode) {
float scale = canvas.getScale();
canvas.pan(-(int)((e.getX() - dragX)*scale), -(int)((e.getY() - dragY)*scale));
dragX = e.getX();
dragY = e.getY();
return true;
} else if (dragMode) {
canvas.panToMouse();
return p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, false, 0);
} else if (rightDragMode) {
canvas.panToMouse();
return p.processPointerEvent(getX(e), getY(e), action, meta, true, true, false, false, 0);
} else if (middleDragMode) {
canvas.panToMouse();
return p.processPointerEvent(getX(e), getY(e), action, meta, true, false, true, false, 0);
} else if (inSwiping) {
// Save the coordinates and restore them afterward.
float x = e.getX();
float y = e.getY();
// Set the coordinates to where the swipe began (i.e. where scaling started).
setEventCoordinates(e, xInitialFocus, yInitialFocus);
int numEvents = 0;
while (numEvents < swipeSpeed && numEvents < maxSwipeSpeed) {
if (twoFingerSwipeUp) {
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, true, 0);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
} else if (twoFingerSwipeDown) {
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, true, 1);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
} else if (twoFingerSwipeLeft) {
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, true, 2);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
} else if (twoFingerSwipeRight) {
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, false, true, 3);
p.processPointerEvent(getX(e), getY(e), action, meta, false, false, false, false, 0);
}
numEvents++;
}
// Restore the coordinates so that onScale doesn't get all muddled up.
setEventCoordinates(e, x, y);
} else if (immersiveSwipe) {
// If this is part of swipe that shows the nav bar, consume.
return true;
}
}
break;
case 1:
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
// Here we only prepare for the second click, which we perform on ACTION_POINTER_UP for pointerID==1.
endDragModesAndScrolling();
// Permit sending mouse-down event on long-tap again.
secondPointerWasDown = true;
// Permit right-clicking again.
thirdPointerWasDown = false;
break;
case MotionEvent.ACTION_POINTER_UP:
if (!inSwiping && !inScaling && !thirdPointerWasDown) {
// If user taps with a second finger while first finger is down, then we treat this as
// a right mouse click, but we only effect the click when the second pointer goes up.
// If the user taps with a second and third finger while the first
// finger is down, we treat it as a middle mouse click. We ignore the lifting of the
// second index when the third index has gone down (using the thirdPointerWasDown variable)
// to prevent inadvertent right-clicks when a middle click has been performed.
p.processPointerEvent(getX(e), getY(e), action, meta, true, true, false, false, 0);
// Enter right-drag mode.
rightDragMode = true;
// Now the event must be passed on to the parent class in order to
// end scaling as it was certainly started when the second pointer went down.
}
break;
}
break;
case 2:
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
if (!inScaling) {
// This boolean prevents the right-click from firing simultaneously as a middle button click.
thirdPointerWasDown = true;
p.processPointerEvent(getX(e), getY(e), action, meta, true, false, true, false, 0);
// Enter middle-drag mode.
middleDragMode = true;
}
}
break;
}
scaleGestures.onTouchEvent(e);
return gestures.onTouchEvent(e);
}
/* (non-Javadoc)
* @see com.iiordanov.android.bc.OnScaleGestureListener#onScale(com.iiordanov.android.bc.IBCScaleGestureDetector)
*/
@Override
public boolean onScale(IBCScaleGestureDetector detector) {
boolean consumed = true;
// Get the current focus.
xCurrentFocus = detector.getFocusX();
yCurrentFocus = detector.getFocusY();
// If we haven't started scaling yet, we check whether a swipe is being performed.
// The arbitrary fudge factor may not be the best way to set a tolerance...
if (!inScaling) {
// Start swiping mode only after we've moved away from the initial focal point some distance.
if (!inSwiping) {
if ( (yCurrentFocus < (yInitialFocus - baseSwipeDist)) ||
(yCurrentFocus > (yInitialFocus + baseSwipeDist)) ||
(xCurrentFocus < (xInitialFocus - baseSwipeDist)) ||
(xCurrentFocus > (xInitialFocus + baseSwipeDist)) ) {
inSwiping = true;
xPreviousFocus = xInitialFocus;
yPreviousFocus = yInitialFocus;
}
}
// If in swiping mode, indicate a swipe at regular intervals.
if (inSwiping) {
twoFingerSwipeUp = false;
twoFingerSwipeDown = false;
twoFingerSwipeLeft = false;
twoFingerSwipeRight = false;
if (yCurrentFocus < (yPreviousFocus - baseSwipeDist)) {
twoFingerSwipeDown = true;
xPreviousFocus = xCurrentFocus;
yPreviousFocus = yCurrentFocus;
} else if (yCurrentFocus > (yPreviousFocus + baseSwipeDist)) {
twoFingerSwipeUp = true;
xPreviousFocus = xCurrentFocus;
yPreviousFocus = yCurrentFocus;
} else if (xCurrentFocus < (xPreviousFocus - baseSwipeDist)) {
twoFingerSwipeRight = true;
xPreviousFocus = xCurrentFocus;
yPreviousFocus = yCurrentFocus;
} else if (xCurrentFocus > (xPreviousFocus + baseSwipeDist)) {
twoFingerSwipeLeft = true;
xPreviousFocus = xCurrentFocus;
yPreviousFocus = yCurrentFocus;
} else {
consumed = false;
}
// The faster we swipe, the faster we traverse the screen, and hence, the
// smaller the time-delta between consumed events. We take the reciprocal
// obtain swipeSpeed. If it goes to zero, we set it to at least one.
long elapsedTime = detector.getTimeDelta();
if (elapsedTime < 10) elapsedTime = 10;
swipeSpeed = baseSwipeTime/elapsedTime;
if (swipeSpeed == 0) swipeSpeed = 1;
//if (consumed) Log.d(TAG,"Current swipe speed: " + swipeSpeed);
}
}
if (!inSwiping) {
if ( !inScaling && Math.abs(1.0 - detector.getScaleFactor()) < minScaleFactor ) {
//Log.i(TAG,"Not scaling due to small scale factor.");
consumed = false;
}
if (consumed) {
inScaling = true;
//Log.i(TAG,"Adjust scaling " + detector.getScaleFactor());
if (canvas != null && canvas.scaling != null)
canvas.scaling.adjust(activity, detector.getScaleFactor(), xCurrentFocus, yCurrentFocus);
}
}
return consumed;
}
/* (non-Javadoc)
* @see com.iiordanov.android.bc.OnScaleGestureListener#onScaleBegin(com.iiordanov.android.bc.IBCScaleGestureDetector)
*/
@Override
public boolean onScaleBegin(IBCScaleGestureDetector detector) {
xInitialFocus = detector.getFocusX();
yInitialFocus = detector.getFocusY();
inScaling = false;
scalingJustFinished = false;
// Cancel any swipes that may have been registered last time.
inSwiping = false;
twoFingerSwipeUp = false;
twoFingerSwipeDown = false;
twoFingerSwipeLeft = false;
twoFingerSwipeRight = false;
//Log.i(TAG,"scale begin ("+xInitialFocus+","+yInitialFocus+")");
return true;
}
/* (non-Javadoc)
* @see com.iiordanov.android.bc.OnScaleGestureListener#onScaleEnd(com.iiordanov.android.bc.IBCScaleGestureDetector)
*/
@Override
public void onScaleEnd(IBCScaleGestureDetector detector) {
//Log.i(TAG,"scale end");
inScaling = false;
inSwiping = false;
scalingJustFinished = true;
}
private static int convertTrackballDelta(double delta) {
return (int) Math.pow(Math.abs(delta) * 6.01, 2.5) * (delta < 0.0 ? -1 : 1);
}
boolean trackballMouse(MotionEvent evt) {
int dx = convertTrackballDelta(evt.getX());
int dy = convertTrackballDelta(evt.getY());
switch (evt.getAction()) {
case MotionEvent.ACTION_DOWN:
trackballButtonDown = true;
break;
case MotionEvent.ACTION_UP:
trackballButtonDown = false;
break;
}
RemotePointer pointer = canvas.getPointer();
evt.offsetLocation(pointer.getX() + dx - evt.getX(),
pointer.getY() + dy - evt.getY());
if (pointer.processPointerEvent(evt, trackballButtonDown, false))
return true;
return activity.onTouchEvent(evt);
}
/**
* Returns the sign of the given number.
* @param number the given number
* @return -1 for negative and 1 for positive.
*/
protected float sign (float number) {
return (number > 0) ? 1 : -1;
}
}