File indexing completed on 2024-05-05 04:38:42

0001 /*
0002     SPDX-FileCopyrightText: 2007 Vladimir Prus
0003     SPDX-FileCopyrightText: 2009-2010 David Nolden
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "activetooltip.h"
0009 #include "debug.h"
0010 
0011 #include <QApplication>
0012 #include <QDesktopWidget>
0013 #include <QEvent>
0014 #include <QMenu>
0015 #include <QMouseEvent>
0016 #include <QPalette>
0017 #include <QPoint>
0018 #include <QPointer>
0019 #include <QScreen>
0020 #include <QStyleOption>
0021 #include <QStylePainter>
0022 
0023 #include <limits>
0024 
0025 using namespace KDevelop;
0026 
0027 namespace {
0028 
0029 class ActiveToolTipManager : public QObject
0030 {
0031     Q_OBJECT
0032 
0033 public Q_SLOTS:
0034     void doVisibility();
0035 
0036 public:
0037     using ToolTipPriorityMap = QMultiMap<float, QPair<QPointer<ActiveToolTip>, QString>>;
0038     ToolTipPriorityMap registeredToolTips;
0039 };
0040 
0041 ActiveToolTipManager* manager()
0042 {
0043     static ActiveToolTipManager m;
0044     return &m;
0045 }
0046 
0047 QWidget* masterWidget(QWidget* w)
0048 {
0049     while (w && w->parent() && qobject_cast<QWidget*>(w->parent())) {
0050         w = qobject_cast<QWidget*>(w->parent());
0051     }
0052     return w;
0053 }
0054 
0055 void ActiveToolTipManager::doVisibility()
0056 {
0057     bool exclusive = false;
0058     int lastBottomPosition = -1;
0059     int lastLeftPosition = -1;
0060     QRect fullGeometry; //Geometry of all visible tooltips together
0061 
0062     for (auto it = registeredToolTips.constBegin(); it != registeredToolTips.constEnd(); ++it) {
0063         QPointer<ActiveToolTip> w = (*it).first;
0064         if (w) {
0065             if (exclusive) {
0066                 (w.data())->hide();
0067             } else {
0068                 QRect geom = (w.data())->geometry();
0069                 if ((w.data())->geometry().top() < lastBottomPosition) {
0070                     geom.moveTop(lastBottomPosition);
0071                 }
0072                 if (lastLeftPosition != -1) {
0073                     geom.moveLeft(lastLeftPosition);
0074                 }
0075 
0076                 (w.data())->setGeometry(geom);
0077 
0078                 lastBottomPosition = (w.data())->geometry().bottom();
0079                 lastLeftPosition = (w.data())->geometry().left();
0080 
0081                 if (it == registeredToolTips.constBegin()) {
0082                     fullGeometry = (w.data())->geometry();
0083                 } else {
0084                     fullGeometry = fullGeometry.united((w.data())->geometry());
0085                 }
0086             }
0087             if (it.key() == 0) {
0088                 exclusive = true;
0089             }
0090         }
0091     }
0092 
0093     if (!fullGeometry.isEmpty()) {
0094         QRect oldFullGeometry = fullGeometry;
0095         const auto *screen = QGuiApplication::screenAt(fullGeometry.topLeft());
0096         if (!screen) {
0097             screen = qApp->primaryScreen();
0098             qWarning() << "failed to find screen:" << fullGeometry << "fallback primary geometry:" << screen->geometry();
0099         }
0100         QRect screenGeometry = screen->geometry();
0101         if (fullGeometry.bottom() > screenGeometry.bottom()) {
0102             //Move up, avoiding the mouse-cursor
0103             fullGeometry.moveBottom(fullGeometry.top() - 10);
0104             if (fullGeometry.adjusted(-20, -20, 20, 20).contains(QCursor::pos())) {
0105                 fullGeometry.moveBottom(QCursor::pos().y() - 20);
0106             }
0107         }
0108         if (fullGeometry.right() > screenGeometry.right()) {
0109             //Move to left, avoiding the mouse-cursor
0110             fullGeometry.moveRight(fullGeometry.left() - 10);
0111             if (fullGeometry.adjusted(-20, -20, 20, 20).contains(QCursor::pos())) {
0112                 fullGeometry.moveRight(QCursor::pos().x() - 20);
0113             }
0114         }
0115         // Now fit this to screen
0116         if (fullGeometry.left() < 0) {
0117             fullGeometry.setLeft(0);
0118         }
0119         if (fullGeometry.top() < 0) {
0120             fullGeometry.setTop(0);
0121         }
0122 
0123         QPoint offset = fullGeometry.topLeft() - oldFullGeometry.topLeft();
0124         if (!offset.isNull()) {
0125             for (auto& toolTipData : qAsConst(registeredToolTips)) {
0126                 auto& toolTip = toolTipData.first;
0127                 if (toolTip) {
0128                     toolTip->move(toolTip->pos() + offset);
0129                 }
0130             }
0131         }
0132     }
0133 
0134     //Always include the mouse cursor in the full geometry, to avoid
0135     //closing the tooltip unexpectedly
0136     fullGeometry = fullGeometry.united(QRect(QCursor::pos(), QCursor::pos()));
0137 
0138     //Set bounding geometry, and remove old tooltips
0139     for (auto it = registeredToolTips.begin(); it != registeredToolTips.end();) {
0140         if (!it->first) {
0141             it = registeredToolTips.erase(it);
0142         } else {
0143             it->first.data()->setBoundingGeometry(fullGeometry);
0144             ++it;
0145         }
0146     }
0147 
0148     //Final step: Show tooltips
0149     for (const auto& tooltip : qAsConst(registeredToolTips)) {
0150         if (tooltip.first.data() && masterWidget(tooltip.first.data())->isActiveWindow()) {
0151             tooltip.first.data()->show();
0152         }
0153         if (exclusive) {
0154             break;
0155         }
0156     }
0157 }
0158 
0159 }
0160 
0161 namespace KDevelop {
0162 class ActiveToolTipPrivate
0163 {
0164 public:
0165     QRect rect_;
0166     QRect handleRect_;
0167     QVector<QPointer<QObject>> friendWidgets_;
0168 };
0169 }
0170 
0171 ActiveToolTip::ActiveToolTip(QWidget* parent, const QPoint& position)
0172     : QWidget(parent, Qt::ToolTip)
0173     , d_ptr(new ActiveToolTipPrivate)
0174 {
0175     Q_D(ActiveToolTip);
0176 
0177     Q_ASSERT(parent);
0178     setMouseTracking(true);
0179     d->rect_ = QRect(position, position);
0180     d->rect_.adjust(-10, -10, 10, 10);
0181     move(position);
0182 
0183     QPalette p;
0184 
0185     // adjust background color to use tooltip colors
0186     p.setColor(backgroundRole(), p.color(QPalette::ToolTipBase));
0187     p.setColor(QPalette::Base, p.color(QPalette::ToolTipBase));
0188 
0189     // adjust foreground color to use tooltip colors
0190     p.setColor(foregroundRole(), p.color(QPalette::ToolTipText));
0191     p.setColor(QPalette::Text, p.color(QPalette::ToolTipText));
0192     setPalette(p);
0193 
0194     setWindowFlags(Qt::WindowDoesNotAcceptFocus | windowFlags());
0195 
0196     qApp->installEventFilter(this);
0197 }
0198 
0199 ActiveToolTip::~ActiveToolTip() = default;
0200 
0201 bool ActiveToolTip::eventFilter(QObject* object, QEvent* e)
0202 {
0203     Q_D(ActiveToolTip);
0204 
0205     switch (e->type()) {
0206     case QEvent::MouseMove:
0207         if (underMouse() || insideThis(object)) {
0208             return false;
0209         } else {
0210             QPoint globalPos = static_cast<QMouseEvent*>(e)->globalPos();
0211             QRect mergedRegion = d->rect_.united(d->handleRect_);
0212 
0213             if (mergedRegion.contains(globalPos)) {
0214                 return false;
0215             }
0216             close();
0217         }
0218         break;
0219 
0220     case QEvent::WindowActivate:
0221         if (insideThis(object)) {
0222             return false;
0223         }
0224         close();
0225         break;
0226 
0227     case QEvent::WindowBlocked:
0228         // Modal dialog activated somewhere, it is the only case where a cursor
0229         // move may be missed and the popup has to be force-closed
0230         close();
0231         break;
0232 
0233     default:
0234         break;
0235     }
0236     return false;
0237 }
0238 
0239 void ActiveToolTip::addFriendWidget(QWidget* widget)
0240 {
0241     Q_D(ActiveToolTip);
0242 
0243     d->friendWidgets_.append(( QObject* )widget);
0244 }
0245 
0246 bool ActiveToolTip::insideThis(QObject* object)
0247 {
0248     Q_D(ActiveToolTip);
0249 
0250     while (object) {
0251         if (qobject_cast<QMenu*>(object)) {
0252             return true;
0253         }
0254 
0255         if (object == this || object == ( QObject* )this->windowHandle() || d->friendWidgets_.contains(object)) {
0256             return true;
0257         }
0258         object = object->parent();
0259     }
0260 
0261     // If the object clicked is inside a QQuickWidget, its parent is null even
0262     // if it is part of a tool-tip. This check ensures that a tool-tip is never
0263     // closed while the mouse is in it
0264     return underMouse();
0265 }
0266 
0267 void ActiveToolTip::showEvent(QShowEvent*)
0268 {
0269     adjustRect();
0270 }
0271 
0272 void ActiveToolTip::resizeEvent(QResizeEvent*)
0273 {
0274     adjustRect();
0275 
0276     // set mask from style
0277     QStyleOptionFrame opt;
0278     opt.init(this);
0279 
0280     QStyleHintReturnMask mask;
0281     if (style()->styleHint(QStyle::SH_ToolTip_Mask, &opt, this, &mask) && !mask.region.isEmpty()) {
0282         setMask(mask.region);
0283     }
0284 
0285     emit resized();
0286 }
0287 
0288 void ActiveToolTip::paintEvent(QPaintEvent* event)
0289 {
0290     QStylePainter painter(this);
0291     painter.setClipRegion(event->region());
0292     QStyleOptionFrame opt;
0293     opt.init(this);
0294     painter.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
0295 }
0296 
0297 void ActiveToolTip::setHandleRect(const QRect& rect)
0298 {
0299     Q_D(ActiveToolTip);
0300 
0301     d->handleRect_ = rect;
0302 }
0303 
0304 void ActiveToolTip::adjustRect()
0305 {
0306     Q_D(ActiveToolTip);
0307 
0308     // For tooltip widget, geometry() returns global coordinates.
0309     QRect r = geometry();
0310     r.adjust(-10, -10, 10, 10);
0311     d->rect_ = r;
0312 }
0313 
0314 void ActiveToolTip::setBoundingGeometry(const QRect& geometry)
0315 {
0316     Q_D(ActiveToolTip);
0317 
0318     d->rect_ = geometry;
0319     d->rect_.adjust(-10, -10, 10, 10);
0320 }
0321 
0322 void ActiveToolTip::showToolTip(ActiveToolTip* tooltip, float priority, const QString& uniqueId)
0323 {
0324     auto& registeredToolTips = manager()->registeredToolTips;
0325     if (!uniqueId.isEmpty()) {
0326         for (const auto& tooltip : qAsConst(registeredToolTips)) {
0327             if (tooltip.second == uniqueId) {
0328                 delete tooltip.first.data();
0329             }
0330         }
0331     }
0332 
0333     registeredToolTips.insert(priority, qMakePair(QPointer<ActiveToolTip>(tooltip), uniqueId));
0334 
0335     connect(tooltip, &ActiveToolTip::resized,
0336             manager(), &ActiveToolTipManager::doVisibility);
0337     QMetaObject::invokeMethod(manager(), "doVisibility", Qt::QueuedConnection);
0338 }
0339 
0340 void ActiveToolTip::closeEvent(QCloseEvent* event)
0341 {
0342     QWidget::closeEvent(event);
0343     deleteLater();
0344 }
0345 
0346 #include "activetooltip.moc"
0347 #include "moc_activetooltip.cpp"