/* ============================================================================== 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 { TooltipWindow::TooltipWindow (Component* parentComp, int delayMs) : Component ("tooltip"), millisecondsBeforeTipAppears (delayMs) { setAlwaysOnTop (true); setOpaque (true); setAccessible (false); if (parentComp != nullptr) parentComp->addChildComponent (this); auto& desktop = Desktop::getInstance(); if (desktop.getMainMouseSource().canHover()) { desktop.addGlobalMouseListener (this); startTimer (123); } } TooltipWindow::~TooltipWindow() { hideTip(); Desktop::getInstance().removeGlobalMouseListener (this); } void TooltipWindow::setMillisecondsBeforeTipAppears (const int newTimeMs) noexcept { millisecondsBeforeTipAppears = newTimeMs; } void TooltipWindow::paint (Graphics& g) { getLookAndFeel().drawTooltip (g, tipShowing, getWidth(), getHeight()); } void TooltipWindow::mouseEnter (const MouseEvent& e) { if (e.eventComponent == this) hideTip(); } void TooltipWindow::mouseDown (const MouseEvent&) { if (isVisible()) dismissalMouseEventOccurred = true; } void TooltipWindow::mouseWheelMove (const MouseEvent&, const MouseWheelDetails&) { if (isVisible()) dismissalMouseEventOccurred = true; } void TooltipWindow::updatePosition (const String& tip, Point pos, Rectangle parentArea) { setBounds (getLookAndFeel().getTooltipBounds (tip, pos, parentArea)); setVisible (true); } #if JUCE_DEBUG static Array activeTooltipWindows; #endif void TooltipWindow::displayTip (Point screenPos, const String& tip) { jassert (tip.isNotEmpty()); displayTipInternal (screenPos, tip, ShownManually::yes); } void TooltipWindow::displayTipInternal (Point screenPos, const String& tip, ShownManually shownManually) { if (! reentrant) { ScopedValueSetter setter (reentrant, true, false); if (tipShowing != tip) { tipShowing = tip; repaint(); } if (auto* parent = getParentComponent()) { updatePosition (tip, parent->getLocalPoint (nullptr, screenPos), parent->getLocalBounds()); } else { const auto physicalPos = ScalingHelpers::scaledScreenPosToUnscaled (screenPos); const auto scaledPos = ScalingHelpers::unscaledScreenPosToScaled (*this, physicalPos); updatePosition (tip, scaledPos, Desktop::getInstance().getDisplays().getDisplayForPoint (screenPos)->userArea); addToDesktop (ComponentPeer::windowHasDropShadow | ComponentPeer::windowIsTemporary | ComponentPeer::windowIgnoresKeyPresses | ComponentPeer::windowIgnoresMouseClicks); } #if JUCE_DEBUG activeTooltipWindows.addIfNotAlreadyThere (this); auto* parent = getParentComponent(); for (auto* w : activeTooltipWindows) { if (w != nullptr && w != this && w->tipShowing == tipShowing && w->getParentComponent() == parent) { // Looks like you have more than one TooltipWindow showing the same tip.. // Be careful not to create more than one instance of this class with the // same parent component! jassertfalse; } } #endif toFront (false); manuallyShownTip = shownManually == ShownManually::yes ? tip : String(); dismissalMouseEventOccurred = false; } } String TooltipWindow::getTipFor (Component& c) { if (isForegroundOrEmbeddedProcess (&c) && ! ModifierKeys::currentModifiers.isAnyMouseButtonDown()) { if (auto* ttc = dynamic_cast (&c)) if (! c.isCurrentlyBlockedByAnotherModalComponent()) return ttc->getTooltip(); } return {}; } void TooltipWindow::hideTip() { if (isVisible() && ! reentrant) { tipShowing = {}; manuallyShownTip = {}; dismissalMouseEventOccurred = false; removeFromDesktop(); setVisible (false); lastHideTime = Time::getApproximateMillisecondCounter(); #if JUCE_DEBUG activeTooltipWindows.removeAllInstancesOf (this); #endif } } float TooltipWindow::getDesktopScaleFactor() const { if (lastComponentUnderMouse != nullptr) return Component::getApproximateScaleFactorForComponent (lastComponentUnderMouse); return Component::getDesktopScaleFactor(); } std::unique_ptr TooltipWindow::createAccessibilityHandler() { return createIgnoredAccessibilityHandler (*this); } void TooltipWindow::timerCallback() { const auto mouseSource = Desktop::getInstance().getMainMouseSource(); auto* newComp = mouseSource.isTouch() ? nullptr : mouseSource.getComponentUnderMouse(); if (manuallyShownTip.isNotEmpty()) { if (dismissalMouseEventOccurred || newComp == nullptr) hideTip(); return; } if (newComp == nullptr || getParentComponent() == nullptr || newComp->getPeer() == getPeer()) { const auto newTip = newComp != nullptr ? getTipFor (*newComp) : String(); const auto mousePos = mouseSource.getScreenPosition(); const auto mouseMovedQuickly = (mousePos.getDistanceFrom (lastMousePos) > 12); lastMousePos = mousePos; const auto tipChanged = (newTip != lastTipUnderMouse || newComp != lastComponentUnderMouse); const auto now = Time::getApproximateMillisecondCounter(); lastComponentUnderMouse = newComp; lastTipUnderMouse = newTip; if (tipChanged || dismissalMouseEventOccurred || mouseMovedQuickly) lastCompChangeTime = now; const auto showTip = [this, &mouseSource, &mousePos, &newTip] { if (mouseSource.getLastMouseDownPosition() != lastMousePos) displayTipInternal (mousePos.roundToInt(), newTip, ShownManually::no); }; if (isVisible() || now < lastHideTime + 500) { // if a tip is currently visible (or has just disappeared), update to a new one // immediately if needed.. if (newComp == nullptr || dismissalMouseEventOccurred || newTip.isEmpty()) hideTip(); else if (tipChanged) showTip(); } else { // if there isn't currently a tip, but one is needed, only let it appear after a timeout if (newTip.isNotEmpty() && newTip != tipShowing && now > lastCompChangeTime + (uint32) millisecondsBeforeTipAppears) { showTip(); } } } } } // namespace juce