/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::detail { class MouseInputSourceImpl : private AsyncUpdater { public: using SH = ScalingHelpers; MouseInputSourceImpl (int i, MouseInputSource::InputSourceType type) : index (i), inputType (type) {} //============================================================================== bool isDragging() const noexcept { return buttonState.isAnyMouseButtonDown(); } Component* getComponentUnderMouse() const noexcept { return componentUnderMouse.get(); } ModifierKeys getCurrentModifiers() const noexcept { return ModifierKeys::getCurrentModifiers() .withoutMouseButtons() .withFlags (buttonState.getRawFlags()); } ComponentPeer* getPeer() noexcept { if (! ComponentPeer::isValidPeer (lastPeer)) lastPeer = nullptr; return lastPeer; } static Component* findComponentAt (Point screenPos, ComponentPeer* peer) { if (! ComponentPeer::isValidPeer (peer)) return nullptr; auto relativePos = SH::unscaledScreenPosToScaled (peer->getComponent(), peer->globalToLocal (screenPos)); auto& comp = peer->getComponent(); // (the contains() call is needed to test for overlapping desktop windows) if (comp.contains (relativePos)) return comp.getComponentAt (relativePos); return nullptr; } Point getScreenPosition() const noexcept { // This needs to return the live position if possible, but it mustn't update the lastScreenPos // value, because that can cause continuity problems. return SH::unscaledScreenPosToScaled (getRawScreenPosition()); } Point getRawScreenPosition() const noexcept { return unboundedMouseOffset + (inputType != MouseInputSource::InputSourceType::touch ? MouseInputSource::getCurrentRawMousePosition() : lastPointerState.position); } void setScreenPosition (Point p) { MouseInputSource::setRawMousePosition (SH::scaledScreenPosToUnscaled (p)); } //============================================================================== #if JUCE_DUMP_MOUSE_EVENTS #define JUCE_MOUSE_EVENT_DBG(desc, screenPos) DBG ("Mouse " << desc << " #" << index \ << ": " << SH::screenPosToLocalPos (comp, screenPos).toString() \ << " - Comp: " << String::toHexString ((pointer_sized_int) &comp)); #else #define JUCE_MOUSE_EVENT_DBG(desc, screenPos) #endif void sendMouseEnter (Component& comp, const detail::PointerState& pointerState, Time time) { JUCE_MOUSE_EVENT_DBG ("enter", pointerState.position) Component::internalMouseEnter (&comp, MouseInputSource (this), SH::screenPosToLocalPos (comp, pointerState.position), time); } void sendMouseExit (Component& comp, const detail::PointerState& pointerState, Time time) { JUCE_MOUSE_EVENT_DBG ("exit", pointerState.position) Component::internalMouseExit (&comp, MouseInputSource (this), SH::screenPosToLocalPos (comp, pointerState.position), time); } void sendMouseMove (Component& comp, const detail::PointerState& pointerState, Time time) { JUCE_MOUSE_EVENT_DBG ("move", pointerState.position) Component::internalMouseMove (&comp, MouseInputSource (this), SH::screenPosToLocalPos (comp, pointerState.position), time); } void sendMouseDown (Component& comp, const detail::PointerState& pointerState, Time time) { JUCE_MOUSE_EVENT_DBG ("down", pointerState.position) Component::internalMouseDown (&comp, MouseInputSource (this), pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)), time); } void sendMouseDrag (Component& comp, const detail::PointerState& pointerState, Time time) { JUCE_MOUSE_EVENT_DBG ("drag", pointerState.position) Component::internalMouseDrag (&comp, MouseInputSource (this), pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)), time); } void sendMouseUp (Component& comp, const detail::PointerState& pointerState, Time time, ModifierKeys oldMods) { JUCE_MOUSE_EVENT_DBG ("up", pointerState.position) Component::internalMouseUp (&comp, MouseInputSource (this), pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)), time, oldMods); } void sendMouseWheel (Component& comp, Point screenPos, Time time, const MouseWheelDetails& wheel) { JUCE_MOUSE_EVENT_DBG ("wheel", screenPos) Component::internalMouseWheel (&comp, MouseInputSource (this), SH::screenPosToLocalPos (comp, screenPos), time, wheel); } void sendMagnifyGesture (Component& comp, Point screenPos, Time time, float amount) { JUCE_MOUSE_EVENT_DBG ("magnify", screenPos) Component::internalMagnifyGesture (&comp, MouseInputSource (this), SH::screenPosToLocalPos (comp, screenPos), time, amount); } #undef JUCE_MOUSE_EVENT_DBG //============================================================================== // (returns true if the button change caused a modal event loop) bool setButtons (const detail::PointerState& pointerState, Time time, ModifierKeys newButtonState) { if (buttonState == newButtonState) return false; // (avoid sending a spurious mouse-drag when we receive a mouse-up) if (! (isDragging() && ! newButtonState.isAnyMouseButtonDown())) setPointerState (pointerState, time, false); // (ignore secondary clicks when there's already a button down) if (buttonState.isAnyMouseButtonDown() == newButtonState.isAnyMouseButtonDown()) { buttonState = newButtonState; return false; } auto lastCounter = mouseEventCounter; if (buttonState.isAnyMouseButtonDown()) { if (auto* current = getComponentUnderMouse()) { auto oldMods = getCurrentModifiers(); buttonState = newButtonState; // must change this before calling sendMouseUp, in case it runs a modal loop sendMouseUp (*current, pointerState.withPositionOffset (unboundedMouseOffset), time, oldMods); if (lastCounter != mouseEventCounter) return true; // if a modal loop happened, then newButtonState is no longer valid } enableUnboundedMouseMovement (false, false); } buttonState = newButtonState; if (buttonState.isAnyMouseButtonDown()) { Desktop::getInstance().incrementMouseClickCounter(); if (auto* current = getComponentUnderMouse()) { registerMouseDown (pointerState.position, time, *current, buttonState, inputType == MouseInputSource::InputSourceType::touch); sendMouseDown (*current, pointerState, time); } } return lastCounter != mouseEventCounter; } void setComponentUnderMouse (Component* newComponent, const detail::PointerState& pointerState, Time time) { auto* current = getComponentUnderMouse(); if (newComponent != current) { WeakReference safeNewComp (newComponent); auto originalButtonState = buttonState; if (current != nullptr) { WeakReference safeOldComp (current); setButtons (pointerState, time, ModifierKeys()); if (auto oldComp = safeOldComp.get()) { componentUnderMouse = safeNewComp; sendMouseExit (*oldComp, pointerState, time); } buttonState = originalButtonState; } componentUnderMouse = safeNewComp.get(); current = safeNewComp.get(); if (current != nullptr) sendMouseEnter (*current, pointerState, time); revealCursor (false); setButtons (pointerState, time, originalButtonState); } } void setPeer (ComponentPeer& newPeer, const detail::PointerState& pointerState, Time time) { if (&newPeer != lastPeer && ( findComponentAt (pointerState.position, &newPeer) != nullptr || findComponentAt (pointerState.position, lastPeer) == nullptr)) { setComponentUnderMouse (nullptr, pointerState, time); lastPeer = &newPeer; setComponentUnderMouse (findComponentAt (pointerState.position, getPeer()), pointerState, time); } } void setPointerState (const detail::PointerState& newPointerState, Time time, bool forceUpdate) { const auto& newScreenPos = newPointerState.position; if (! isDragging()) setComponentUnderMouse (findComponentAt (newScreenPos, getPeer()), newPointerState, time); if ((newPointerState != lastPointerState) || forceUpdate) { cancelPendingUpdate(); lastPointerState = newPointerState; if (auto* current = getComponentUnderMouse()) { if (isDragging()) { registerMouseDrag (newScreenPos); sendMouseDrag (*current, newPointerState.withPositionOffset (unboundedMouseOffset), time); if (isUnboundedMouseModeOn) handleUnboundedDrag (*current); } else { sendMouseMove (*current, newPointerState, time); } } revealCursor (false); } } //============================================================================== void handleEvent (ComponentPeer& newPeer, Point positionWithinPeer, Time time, const ModifierKeys newMods, float newPressure, float newOrientation, PenDetails pen) { lastTime = time; ++mouseEventCounter; const auto pointerState = detail::PointerState().withPosition (newPeer.localToGlobal (positionWithinPeer)) .withPressure (newPressure) .withOrientation (newOrientation) .withRotation (MouseInputSource::defaultRotation) .withTiltX (pen.tiltX) .withTiltY (pen.tiltY); if (isDragging() && newMods.isAnyMouseButtonDown()) { setPointerState (pointerState, time, false); } else { setPeer (newPeer, pointerState, time); if (auto* peer = getPeer()) { if (setButtons (pointerState, time, newMods)) return; // some modal events have been dispatched, so the current event is now out-of-date peer = getPeer(); if (peer != nullptr) setPointerState (pointerState, time, false); } } } Component* getTargetForGesture (ComponentPeer& peer, Point positionWithinPeer, Time time, Point& screenPos) { lastTime = time; ++mouseEventCounter; screenPos = peer.localToGlobal (positionWithinPeer); const auto pointerState = lastPointerState.withPosition (screenPos); setPeer (peer, pointerState, time); setPointerState (pointerState, time, false); triggerFakeMove(); return getComponentUnderMouse(); } void handleWheel (ComponentPeer& peer, Point positionWithinPeer, Time time, const MouseWheelDetails& wheel) { Desktop::getInstance().incrementMouseWheelCounter(); Point screenPos; // This will make sure that when the wheel spins in its inertial phase, any events // continue to be sent to the last component that the mouse was over when it was being // actively controlled by the user. This avoids confusion when scrolling through nested // scrollable components. if (lastNonInertialWheelTarget == nullptr || ! wheel.isInertial) lastNonInertialWheelTarget = getTargetForGesture (peer, positionWithinPeer, time, screenPos); else screenPos = peer.localToGlobal (positionWithinPeer); if (auto target = lastNonInertialWheelTarget.get()) sendMouseWheel (*target, screenPos, time, wheel); } void handleMagnifyGesture (ComponentPeer& peer, Point positionWithinPeer, Time time, const float scaleFactor) { Point screenPos; if (auto* current = getTargetForGesture (peer, positionWithinPeer, time, screenPos)) sendMagnifyGesture (*current, screenPos, time, scaleFactor); } //============================================================================== Time getLastMouseDownTime() const noexcept { return mouseDowns[0].time; } Point getLastMouseDownPosition() const noexcept { return SH::unscaledScreenPosToScaled (mouseDowns[0].position); } int getNumberOfMultipleClicks() const noexcept { int numClicks = 1; if (! isLongPressOrDrag()) { for (int i = 1; i < numElementsInArray (mouseDowns); ++i) { if (mouseDowns[0].canBePartOfMultipleClickWith (mouseDowns[i], MouseEvent::getDoubleClickTimeout() * jmin (i, 2))) ++numClicks; else break; } } return numClicks; } bool isLongPressOrDrag() const noexcept { return movedSignificantly || lastTime > (mouseDowns[0].time + RelativeTime::milliseconds (300)); } bool hasMovedSignificantlySincePressed() const noexcept { return movedSignificantly; } // Deprecated method bool hasMouseMovedSignificantlySincePressed() const noexcept { return isLongPressOrDrag(); } //============================================================================== void triggerFakeMove() { triggerAsyncUpdate(); } void handleAsyncUpdate() override { setPointerState (lastPointerState, jmax (lastTime, Time::getCurrentTime()), true); } //============================================================================== void enableUnboundedMouseMovement (bool enable, bool keepCursorVisibleUntilOffscreen) { enable = enable && isDragging(); isCursorVisibleUntilOffscreen = keepCursorVisibleUntilOffscreen; if (enable != isUnboundedMouseModeOn) { if ((! enable) && ((! isCursorVisibleUntilOffscreen) || ! unboundedMouseOffset.isOrigin())) { // when released, return the mouse to within the component's bounds if (auto* current = getComponentUnderMouse()) setScreenPosition (current->getScreenBounds().toFloat() .getConstrainedPoint (SH::unscaledScreenPosToScaled (lastPointerState.position))); } isUnboundedMouseModeOn = enable; unboundedMouseOffset = {}; revealCursor (true); } } void handleUnboundedDrag (Component& current) { auto componentScreenBounds = SH::scaledScreenPosToUnscaled (current.getParentMonitorArea() .reduced (2, 2) .toFloat()); if (! componentScreenBounds.contains (lastPointerState.position)) { auto componentCentre = current.getScreenBounds().toFloat().getCentre(); unboundedMouseOffset += (lastPointerState.position - SH::scaledScreenPosToUnscaled (componentCentre)); setScreenPosition (componentCentre); } else if (isCursorVisibleUntilOffscreen && (! unboundedMouseOffset.isOrigin()) && componentScreenBounds.contains (lastPointerState.position + unboundedMouseOffset)) { MouseInputSource::setRawMousePosition (lastPointerState.position + unboundedMouseOffset); unboundedMouseOffset = {}; } } //============================================================================== void showMouseCursor (MouseCursor cursor, bool forcedUpdate) { if (isUnboundedMouseModeOn && ((! unboundedMouseOffset.isOrigin()) || ! isCursorVisibleUntilOffscreen)) { cursor = MouseCursor::NoCursor; forcedUpdate = true; } if (forcedUpdate || cursor.getHandle() != currentCursorHandle) { currentCursorHandle = cursor.getHandle(); cursor.showInWindow (getPeer()); } } void hideCursor() { showMouseCursor (MouseCursor::NoCursor, true); } void revealCursor (bool forcedUpdate) { MouseCursor mc (MouseCursor::NormalCursor); if (auto* current = getComponentUnderMouse()) mc = current->getLookAndFeel().getMouseCursorFor (*current); showMouseCursor (mc, forcedUpdate); } //============================================================================== const int index; const MouseInputSource::InputSourceType inputType; Point unboundedMouseOffset; // NB: these are unscaled coords detail::PointerState lastPointerState; ModifierKeys buttonState; bool isUnboundedMouseModeOn = false, isCursorVisibleUntilOffscreen = false; private: WeakReference componentUnderMouse, lastNonInertialWheelTarget; ComponentPeer* lastPeer = nullptr; void* currentCursorHandle = nullptr; int mouseEventCounter = 0; struct RecentMouseDown { RecentMouseDown() = default; Point position; Time time; ModifierKeys buttons; uint32 peerID = 0; bool isTouch = false; bool canBePartOfMultipleClickWith (const RecentMouseDown& other, int maxTimeBetweenMs) const noexcept { return time - other.time < RelativeTime::milliseconds (maxTimeBetweenMs) && std::abs (position.x - other.position.x) < (float) getPositionToleranceForInputType() && std::abs (position.y - other.position.y) < (float) getPositionToleranceForInputType() && buttons == other.buttons && peerID == other.peerID; } int getPositionToleranceForInputType() const noexcept { return isTouch ? 25 : 8; } }; RecentMouseDown mouseDowns[4]; Time lastTime; bool movedSignificantly = false; void registerMouseDown (Point screenPos, Time time, Component& component, const ModifierKeys modifiers, bool isTouchSource) noexcept { for (int i = numElementsInArray (mouseDowns); --i > 0;) mouseDowns[i] = mouseDowns[i - 1]; mouseDowns[0].position = screenPos; mouseDowns[0].time = time; mouseDowns[0].buttons = modifiers.withOnlyMouseButtons(); mouseDowns[0].isTouch = isTouchSource; if (auto* peer = component.getPeer()) mouseDowns[0].peerID = peer->getUniqueID(); else mouseDowns[0].peerID = 0; movedSignificantly = false; lastNonInertialWheelTarget = nullptr; } void registerMouseDrag (Point screenPos) noexcept { movedSignificantly = movedSignificantly || mouseDowns[0].position.getDistanceFrom (screenPos) >= 4; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseInputSourceImpl) }; } // namespace juce::detail