/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 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::currentModifiers .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) comp.internalMouseEnter (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) comp.internalMouseExit (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) comp.internalMouseMove (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) comp.internalMouseDown (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) comp.internalMouseDrag (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) comp.internalMouseUp (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) comp.internalMouseWheel (MouseInputSource (this), SH::screenPosToLocalPos (comp, screenPos), time, wheel); } void sendMagnifyGesture (Component& comp, Point screenPos, Time time, float amount) { JUCE_MOUSE_EVENT_DBG ("magnify", screenPos) comp.internalMagnifyGesture (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