diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -48,9 +48,11 @@ Set two finger tap action Set three finger tap action Set touchpad sensitivity + Set pointer acceleration mousepad_double_tap_key mousepad_triple_tap_key mousepad_sensitivity_key + mousepad_acceleration_profile_key Reverse Scrolling Direction mousepad_scroll_direction @@ -61,6 +63,7 @@ right middle default + medium right middle @@ -73,13 +76,29 @@ Above Default Fastest + + No Acceleration + Weakest + Weaker + Medium + Stronger + Strongest + slowest aboveSlowest default aboveDefault fastest + + noacceleration + weaker + weak + medium + strong + stronger + Connected devices Available devices Remembered devices diff --git a/res/xml/mousepadplugin_preferences.xml b/res/xml/mousepadplugin_preferences.xml --- a/res/xml/mousepadplugin_preferences.xml +++ b/res/xml/mousepadplugin_preferences.xml @@ -30,9 +30,18 @@ android:summary="%s" android:title="@string/mousepad_sensitivity_settings_title" /> + + \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -60,6 +60,9 @@ private GestureDetector mDetector; private MousePadGestureDetector mMousePadGestureDetector; + private PointerAccelerationProfile mPointerAccelerationProfile; + + private PointerAccelerationProfile.MouseDelta mouseDelta; // to be reused on every touch move event KeyListenerView keyListenerView; @@ -111,6 +114,11 @@ String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key), getString(R.string.mousepad_default_sensitivity)); + String accelerationProfileName=prefs.getString(getString(R.string.mousepad_acceleration_profile_key), + getString(R.string.mousepad_default_acceleration_profile)); + + mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName); + doubleTapAction = ClickType.fromString(doubleTapSetting); tripleTapAction = ClickType.fromString(tripleTapSetting); @@ -221,10 +229,16 @@ Device device = service.getDevice(deviceId); MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class); if (mousePadPlugin == null) return; - mousePadPlugin.sendMouseDelta( - (mCurrentX - mPrevX) * displayDpiMultiplier, - (mCurrentY - mPrevY) * displayDpiMultiplier, - mCurrentSensitivity); + + float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity; + float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity; + + // Run the mouse delta through the pointer acceleration profile + mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime()); + mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta); + + mousePadPlugin.sendMouseDelta(mouseDelta.x,mouseDelta.y); + mPrevX = mCurrentX; mPrevY = mCurrentY; }); diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.java --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadPlugin.java @@ -81,15 +81,11 @@ return context.getString(R.string.open_mousepad); } - public void sendMouseDelta(float dx, float dy, float sensitivity) { + public void sendMouseDelta(float dx, float dy) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST); - if (sensitivity <= 0.0001f) { - sensitivity = 1.0f; - } - - np.set("dx", dx * sensitivity); - np.set("dy", dy * sensitivity); + np.set("dx", dx); + np.set("dy", dy); device.sendPacket(np); } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfile.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfile.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfile.java @@ -0,0 +1,29 @@ +package org.kde.kdeconnect.Plugins.MousePadPlugin; + +/* Base class for a pointer acceleration profile. */ +public abstract class PointerAccelerationProfile { + + /* Class representing a mouse delta, a pair of floats.*/ + static class MouseDelta { + public float x, y; + + public MouseDelta() { + this(0,0); + } + + public MouseDelta(float x, float y) { + this.x = x; + this.y = y; + } + } + + /* Touch coordinate deltas are fed through this method. */ + public abstract void touchMoved(float deltaX, float deltaY, long eventTime); + + /* An acceleration profile should 'commit' the processed delta when this method is called. + * The value returned here will be directly sent to the desktop client. + * + * A MouseDelta object can be provided by the caller (or it can be null); + * if not null, subclasses should use and return this object, to reduce object allocations.*/ + public abstract MouseDelta commitAcceleratedMouseDelta(MouseDelta reusedObject); +} diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfileFactory.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfileFactory.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/PointerAccelerationProfileFactory.java @@ -0,0 +1,171 @@ +package org.kde.kdeconnect.Plugins.MousePadPlugin; + + +public class PointerAccelerationProfileFactory { + + /* The simplest profile. Merely adds the mouse deltas without any processing. */ + private static class DefaultProfile extends PointerAccelerationProfile { + float accumulatedX = 0.0f; + float accumulatedY = 0.0f; + + @Override + public void touchMoved(float deltaX, float deltaY, long eventTime) { + accumulatedX += deltaX; + accumulatedY += deltaY; + } + + @Override + public MouseDelta commitAcceleratedMouseDelta(MouseDelta reusedObject) { + MouseDelta result; + if (reusedObject == null) result = new MouseDelta(); + else result = reusedObject; + + result.x = accumulatedX; + result.y = accumulatedY; + accumulatedY = 0; + accumulatedX = 0; + return result; + } + } + + + /* Base class for acceleration profiles that takes touch movement speed into account. + * To calculate the speed, a history of 32 touch events are stored + * and then later used to calculate the speed. */ + private static abstract class SpeedBasedAccelerationProfile extends PointerAccelerationProfile { + float accumulatedX = 0.0f; + float accumulatedY = 0.0f; + + // Higher values will reduce the amount of noise in the speed calculation + // but will also increase latency until the acceleration kicks in. + // 150ms seemed like a nice middle ground. + long freshThreshold = 150; + + private static class TouchDeltaEvent { + float x, y; + long time; + public TouchDeltaEvent(float x, float y, long t) { + this.x = x; + this.y = y; + this.time = t; + } + } + + private TouchDeltaEvent[] touchEventHistory = new TouchDeltaEvent[32]; + + /* add an event to the touchEventHistory array, shifting everything else in the array. */ + private void addHistory(float deltaX, float deltaY, long eventTime) { + for (int i = touchEventHistory.length - 1; i > 0; i--) { + touchEventHistory[i] = touchEventHistory[i - 1]; + } + touchEventHistory[0] = new TouchDeltaEvent(deltaX, deltaY, eventTime); + } + + @Override + public void touchMoved(float deltaX, float deltaY, long eventTime) { + + // To calculate the touch movement speed, + // we iterate through the touchEventHistory array, + // adding up the distance moved for each event. + // Breaks if a touchEventHistory entry is too "stale". + float distanceMoved = 0.0f; + long deltaT = 0; + for (int i = 0; i < touchEventHistory.length; i++) { + if (touchEventHistory[i] == null) break; + if (eventTime - touchEventHistory[i].time > freshThreshold) break; + + distanceMoved += (float) Math.sqrt( + touchEventHistory[i].x * touchEventHistory[i].x + + touchEventHistory[i].y * touchEventHistory[i].y); + deltaT = eventTime - touchEventHistory[i].time; + } + + + float multiplier; + + if (deltaT == 0) { + // Edge case when there are no historical touch data to calculate speed from. + multiplier = 0; + } else { + float speed = distanceMoved / (deltaT / 1000.0f); // units: px/sec + + multiplier = calculateMultiplier(speed); + } + + if (multiplier < 0.01f) multiplier = 0.01f; + + accumulatedX += deltaX * multiplier; + accumulatedY += deltaY * multiplier; + + addHistory(deltaX, deltaY, eventTime); + } + + /* Should be overridden by the child class. + * Given the current touch movement speed, this method should return a multiplier + * for the touch delta. ( mouse_delta = touch_delta * multiplier ) */ + abstract float calculateMultiplier(float speed); + + @Override + public MouseDelta commitAcceleratedMouseDelta(MouseDelta reusedObject) { + MouseDelta result; + if (reusedObject == null) result = new MouseDelta(); + else result = reusedObject; + + /* This makes sure that only the integer components of the deltas are sent, + * since the coordinates are converted to integers in the desktop client anyway. + * The leftover fractional part is stored and added later; this makes + * the cursor move much smoother in slow speeds. */ + result.x = (int) accumulatedX; + result.y = (int) accumulatedY; + accumulatedY = accumulatedY % 1.0f; + accumulatedX = accumulatedX % 1.0f; + return result; + } + } + + /* Pointer acceleration with mouse_delta = touch_delta * ( touch_speed ^ exponent ) + * It is similar to x.org server's Polynomial pointer acceleration profile. */ + private static class PolynomialProfile extends SpeedBasedAccelerationProfile { + float exponent; + + public PolynomialProfile(float exponent) { + this.exponent = exponent; + } + + @Override + float calculateMultiplier(float speed) { + // The value 600 was chosen arbitrarily. + return (float) (Math.pow(speed / 600, exponent)); + } + } + + + /* Mimics the behavior of xorg's Power profile. + * Currently not visible to the user since it is rather hard to control. */ + private static class PowerProfile extends SpeedBasedAccelerationProfile { + @Override + float calculateMultiplier(float speed) { + return (float) (Math.pow(2, speed / 1000)) - 1; + } + } + + + public static PointerAccelerationProfile getProfileWithName(String name) { + switch (name) { + case "noacceleration": + return new DefaultProfile(); + case "weaker": + return new PolynomialProfile(0.25f); + case "weak": + return new PolynomialProfile(0.5f); + case "medium": + return new PolynomialProfile(1.0f); + case "strong": + return new PolynomialProfile(1.5f); + case "stronger": + return new PolynomialProfile(2.0f); + default: + return new DefaultProfile(); + } + } +}