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"