/* * Copyright 2008-2011, Clemens Zeidler * Copyright 2022, Haiku, Inc. All rights reserved. * Distributed under the terms of the MIT License. */ #include "movement_maker.h" #include #include #include //#define TRACE_MOVEMENT_MAKER #ifdef TRACE_MOVEMENT_MAKER # define TRACE(x...) dprintf(x) #else # define TRACE(x...) #endif // magic constants #define SYN_WIDTH (4100) #define SYN_HEIGHT (3140) static int32 make_small(float value) { if (value > 0) return (int32)floorf(value); else return (int32)ceilf(value); } void MovementMaker::SetSettings(const touchpad_settings& settings) { fSettings = settings; } void MovementMaker::SetSpecs(const touchpad_specs& specs) { fSpecs = specs; fAreaWidth = fSpecs.areaEndX - fSpecs.areaStartX; fAreaHeight = fSpecs.areaEndY - fSpecs.areaStartY; // calibrated on the synaptics touchpad fSpeed = SYN_WIDTH / fAreaWidth; fSmallMovement = 3 / fSpeed; } void MovementMaker::StartNewMovment() { if (fSettings.scroll_xstepsize <= 0) fSettings.scroll_xstepsize = 1; if (fSettings.scroll_ystepsize <= 0) fSettings.scroll_ystepsize = 1; fMovementMakerStarted = true; scrolling_x = 0; scrolling_y = 0; } void MovementMaker::GetMovement(uint32 posX, uint32 posY) { _GetRawMovement(posX, posY); } void MovementMaker::GetScrolling(uint32 posX, uint32 posY) { int32 stepsX = 0, stepsY = 0; _GetRawMovement(posX, posY); _ComputeAcceleration(fSettings.scroll_acceleration); if (fSettings.scroll_xstepsize > 0) { scrolling_x += xDelta; stepsX = make_small(scrolling_x / fSettings.scroll_xstepsize); scrolling_x -= stepsX * fSettings.scroll_xstepsize; xDelta = stepsX; } else { scrolling_x = 0; xDelta = 0; } if (fSettings.scroll_ystepsize > 0) { scrolling_y += yDelta; stepsY = make_small(scrolling_y / fSettings.scroll_ystepsize); scrolling_y -= stepsY * fSettings.scroll_ystepsize; yDelta = -1 * stepsY; } else { scrolling_y = 0; yDelta = 0; } } void MovementMaker::_GetRawMovement(uint32 posX, uint32 posY) { // calibrated on the synaptics touchpad posX = posX * SYN_WIDTH / fAreaWidth; posY = posY * SYN_HEIGHT / fAreaHeight; const float acceleration = 0.8; const float translation = 12.0; int diff; if (fMovementMakerStarted) { fMovementMakerStarted = false; // init delta tracking fPreviousX = posX; fPreviousY = posY; // deltas are automatically reset } // accumulate delta and store current pos, reset if pos did not change diff = posX - fPreviousX; // lessen the effect of small diffs if ((diff > -fSmallMovement && diff < -1) || (diff > 1 && diff < fSmallMovement)) { diff /= 2; } if (diff == 0) fDeltaSumX = 0; else fDeltaSumX += diff; diff = posY - fPreviousY; // lessen the effect of small diffs if ((diff > -fSmallMovement && diff < -1) || (diff > 1 && diff < fSmallMovement)) { diff /= 2; } if (diff == 0) fDeltaSumY = 0; else fDeltaSumY += diff; fPreviousX = posX; fPreviousY = posY; // compute current delta and reset accumulated delta if // abs() is greater than 1 xDelta = fDeltaSumX / translation; yDelta = fDeltaSumY / translation; if (xDelta > 1.0) { fDeltaSumX = 0.0; xDelta = 1.0 + (xDelta - 1.0) * acceleration; } else if (xDelta < -1.0) { fDeltaSumX = 0.0; xDelta = -1.0 + (xDelta + 1.0) * acceleration; } if (yDelta > 1.0) { fDeltaSumY = 0.0; yDelta = 1.0 + (yDelta - 1.0) * acceleration; } else if (yDelta < -1.0) { fDeltaSumY = 0.0; yDelta = -1.0 + (yDelta + 1.0) * acceleration; } xDelta = make_small(xDelta); yDelta = make_small(yDelta); } void MovementMaker::_ComputeAcceleration(int8 accel_factor) { // acceleration float acceleration = 1; if (accel_factor != 0) { acceleration = 1 + sqrtf(xDelta * xDelta + yDelta * yDelta) * accel_factor / 50.0; } xDelta = make_small(xDelta * acceleration); yDelta = make_small(yDelta * acceleration); } // #pragma mark - #define fTapTimeOUT 200000 TouchpadMovement::TouchpadMovement() { fMovementStarted = false; fScrollingStarted = false; fTapStarted = false; fValidEdgeMotion = false; fDoubleClick = false; } status_t TouchpadMovement::EventToMovement(const touchpad_movement* event, mouse_movement* movement, bigtime_t& repeatTimeout) { if (!movement) return B_ERROR; movement->xdelta = 0; movement->ydelta = 0; movement->buttons = 0; movement->wheel_ydelta = 0; movement->wheel_xdelta = 0; movement->modifiers = 0; movement->clicks = 0; movement->timestamp = system_time(); if ((movement->timestamp - fTapTime) > fTapTimeOUT) { if (fTapStarted) TRACE("TouchpadMovement: tap gesture timed out\n"); fTapStarted = false; if (!fDoubleClick || (movement->timestamp - fTapTime) > 2 * fTapTimeOUT) { fTapClicks = 0; } } if (event->buttons & kLeftButton) { fTapClicks = 0; fTapdragStarted = false; fTapStarted = false; fValidEdgeMotion = false; } if (event->zPressure >= fSpecs.minPressure && event->zPressure < fSpecs.maxPressure && ((event->fingerWidth >= 4 && event->fingerWidth <= 7) || event->fingerWidth == 0 || event->fingerWidth == 1) && (event->xPosition != 0 || event->yPosition != 0)) { // The touch pad is in touch with at least one finger if (!_CheckScrollingToMovement(event, movement)) _MoveToMovement(event, movement); } else _NoTouchToMovement(event, movement); if (fTapdragStarted || fValidEdgeMotion) { // We want the current event to be repeated in 50ms if no other // events occur in the interim. repeatTimeout = 1000 * 50; } else repeatTimeout = B_INFINITE_TIMEOUT; return B_OK; } // in pixel per second const int32 kEdgeMotionSpeed = 200; bool TouchpadMovement::_EdgeMotion(const touchpad_movement *event, mouse_movement *movement, bool validStart) { float xdelta = 0; float ydelta = 0; bigtime_t time = system_time(); if (fLastEdgeMotion != 0) { xdelta = fRestEdgeMotion + kEdgeMotionSpeed * float(time - fLastEdgeMotion) / (1000 * 1000); fRestEdgeMotion = xdelta - int32(xdelta); ydelta = xdelta; } else { fRestEdgeMotion = 0; } bool inXEdge = false; bool inYEdge = false; if (int32(event->xPosition) < fSpecs.areaStartX + fSpecs.edgeMotionWidth) { inXEdge = true; xdelta *= -1; } else if (event->xPosition > uint16( fSpecs.areaEndX - fSpecs.edgeMotionWidth)) { inXEdge = true; } if (int32(event->yPosition) < fSpecs.areaStartY + fSpecs.edgeMotionWidth) { inYEdge = true; ydelta *= -1; } else if (event->yPosition > uint16( fSpecs.areaEndY - fSpecs.edgeMotionWidth)) { inYEdge = true; } // for a edge motion the drag has to be started in the middle of the pad // TODO: this is difficult to understand simplify the code if (inXEdge && validStart) movement->xdelta = make_small(xdelta); if (inYEdge && validStart) movement->ydelta = make_small(ydelta); if (!inXEdge && !inYEdge) fLastEdgeMotion = 0; else fLastEdgeMotion = time; if ((inXEdge || inYEdge) && !validStart) return false; return true; } /*! If a button has been clicked (movement->buttons must be set accordingly), this function updates the fClickCount, as well as the \a movement's clicks field. Also, it sets the button state from movement->buttons. */ void TouchpadMovement::_UpdateButtons(mouse_movement *movement) { // set click count correctly according to double click timeout if (movement->buttons != 0 && fButtonsState == 0) { if (fClickLastTime + click_speed > movement->timestamp) fClickCount++; else fClickCount = 1; fClickLastTime = movement->timestamp; } if (movement->buttons != 0) movement->clicks = fClickCount; fButtonsState = movement->buttons; } void TouchpadMovement::_NoTouchToMovement(const touchpad_movement *event, mouse_movement *movement) { uint32 buttons = event->buttons; if (fMovementStarted) TRACE("TouchpadMovement: no touch event\n"); fScrollingStarted = false; fMovementStarted = false; fLastEdgeMotion = 0; if (fTapdragStarted && (movement->timestamp - fTapTime) < fTapTimeOUT) { buttons = kLeftButton; } // if the movement stopped switch off the tap drag when timeout is expired if ((movement->timestamp - fTapTime) > fTapTimeOUT) { if (fTapdragStarted) TRACE("TouchpadMovement: tap drag gesture timed out\n"); fTapdragStarted = false; fValidEdgeMotion = false; } if (abs(fTapDeltaX) > 15 || abs(fTapDeltaY) > 15) { fTapStarted = false; fTapClicks = 0; } if (fTapStarted || fDoubleClick) { TRACE("TouchpadMovement: tap gesture\n"); fTapClicks++; if (fTapClicks > 1) { TRACE("TouchpadMovement: empty click\n"); buttons = kNoButton; fTapClicks = 0; fDoubleClick = true; } else { buttons = kLeftButton; fTapStarted = false; fTapdragStarted = true; fDoubleClick = false; } } movement->buttons = buttons; _UpdateButtons(movement); } void TouchpadMovement::_MoveToMovement(const touchpad_movement *event, mouse_movement *movement) { bool isStartOfMovement = false; float pressure = 0; TRACE("TouchpadMovement: movement event\n"); if (!fMovementStarted) { isStartOfMovement = true; fMovementStarted = true; StartNewMovment(); } GetMovement(event->xPosition, event->yPosition); movement->xdelta = make_small(xDelta); movement->ydelta = make_small(yDelta); // tap gesture fTapDeltaX += make_small(xDelta); fTapDeltaY += make_small(yDelta); if (fTapdragStarted) { movement->buttons = kLeftButton; movement->clicks = 0; fValidEdgeMotion = _EdgeMotion(event, movement, fValidEdgeMotion); TRACE("TouchpadMovement: tap drag\n"); } else { TRACE("TouchpadMovement: movement set buttons\n"); movement->buttons = event->buttons; } // use only a fraction of pressure range, the max pressure seems to be // to high pressure = 20 * (event->zPressure - fSpecs.minPressure) / (fSpecs.realMaxPressure - fSpecs.minPressure); if (!fTapStarted && isStartOfMovement && fSettings.tapgesture_sensibility > 0. && fSettings.tapgesture_sensibility > (20 - pressure)) { TRACE("TouchpadMovement: tap started\n"); fTapStarted = true; fTapTime = system_time(); fTapDeltaX = 0; fTapDeltaY = 0; } _UpdateButtons(movement); } /*! Checks if this is a scrolling event or not, and also actually does the scrolling work if it is. \return \c true if this was a scrolling event, \c false if not. */ bool TouchpadMovement::_CheckScrollingToMovement(const touchpad_movement *event, mouse_movement *movement) { bool isSideScrollingV = false; bool isSideScrollingH = false; // if a button is pressed don't allow to scroll, we likely be in a drag // action if (fButtonsState != 0) return false; if ((fSpecs.areaEndX - fAreaWidth * fSettings.scroll_rightrange < event->xPosition && !fMovementStarted && fSettings.scroll_rightrange > 0.000001) || fSettings.scroll_rightrange > 0.999999) { isSideScrollingV = true; } if ((fSpecs.areaStartY + fAreaHeight * fSettings.scroll_bottomrange > event->yPosition && !fMovementStarted && fSettings.scroll_bottomrange > 0.000001) || fSettings.scroll_bottomrange > 0.999999) { isSideScrollingH = true; } if ((event->fingerWidth == 0 || event->fingerWidth == 1) && fSettings.scroll_twofinger) { // two finger scrolling is enabled isSideScrollingV = true; isSideScrollingH = fSettings.scroll_twofinger_horizontal; } if (!isSideScrollingV && !isSideScrollingH) { fScrollingStarted = false; return false; } TRACE("TouchpadMovement: scroll event\n"); fTapStarted = false; fTapClicks = 0; fTapdragStarted = false; fValidEdgeMotion = false; if (!fScrollingStarted) { fScrollingStarted = true; StartNewMovment(); } GetScrolling(event->xPosition, event->yPosition); movement->wheel_ydelta = make_small(yDelta); movement->wheel_xdelta = make_small(xDelta); if (isSideScrollingV && !isSideScrollingH) movement->wheel_xdelta = 0; else if (isSideScrollingH && !isSideScrollingV) movement->wheel_ydelta = 0; fButtonsState = movement->buttons; return true; }