1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2008 Fredrik Höglund <fredrik@kde.org> |
4 | SPDX-FileCopyrightText: 2008 Konstantin Heil <konst.heil@stud.uni-heidelberg.de> |
5 | SPDX-FileCopyrightText: 2009 Peter Penz <peter.penz@gmx.at> |
6 | SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvis.angelaccio@kde.org> |
7 | |
8 | SPDX-License-Identifier: LGPL-2.1-or-later |
9 | */ |
10 | |
11 | #include "ktooltipwidget.h" |
12 | |
13 | #include <QPaintEvent> |
14 | #include <QScreen> |
15 | #include <QStyleOptionFrame> |
16 | #include <QStylePainter> |
17 | #include <QTimer> |
18 | #include <QVBoxLayout> |
19 | #include <QWindow> |
20 | |
21 | class KToolTipWidgetPrivate |
22 | { |
23 | public: |
24 | KToolTipWidgetPrivate(KToolTipWidget *parent) |
25 | : q(parent) |
26 | { |
27 | } |
28 | |
29 | void init(); |
30 | void addWidget(QWidget *widget); |
31 | void removeWidget(); |
32 | void show(const QPoint &pos, QWindow *transientParent); |
33 | void storeParent(); |
34 | void restoreParent(); |
35 | QPoint centerBelow(const QRect &rect, QScreen *screen) const; |
36 | |
37 | KToolTipWidget *const q; |
38 | QTimer hideTimer; |
39 | QVBoxLayout *layout = nullptr; |
40 | QWidget *content = nullptr; |
41 | QWidget *contentParent = nullptr; |
42 | }; |
43 | |
44 | void KToolTipWidgetPrivate::init() |
45 | { |
46 | layout = new QVBoxLayout(q); |
47 | |
48 | hideTimer.setSingleShot(true); |
49 | hideTimer.setInterval(500); |
50 | |
51 | QObject::connect(sender: &hideTimer, signal: &QTimer::timeout, context: q, slot: &QWidget::hide); |
52 | |
53 | q->setAttribute(Qt::WA_TranslucentBackground); |
54 | q->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); |
55 | } |
56 | |
57 | void KToolTipWidgetPrivate::addWidget(QWidget *widget) |
58 | { |
59 | removeWidget(); |
60 | content = widget; |
61 | storeParent(); |
62 | layout->addWidget(content); |
63 | QObject::connect(sender: content, signal: &QWidget::destroyed, context: q, slot: &QWidget::hide); |
64 | } |
65 | |
66 | void KToolTipWidgetPrivate::removeWidget() |
67 | { |
68 | layout->removeWidget(w: content); |
69 | restoreParent(); |
70 | } |
71 | |
72 | void KToolTipWidgetPrivate::show(const QPoint &pos, QWindow *transientParent) |
73 | { |
74 | if (pos.isNull()) { |
75 | return; |
76 | } |
77 | |
78 | q->move(pos); |
79 | q->createWinId(); |
80 | q->windowHandle()->setProperty(name: "ENABLE_BLUR_BEHIND_HINT" , value: true); |
81 | q->windowHandle()->setTransientParent(transientParent); |
82 | |
83 | QObject::connect(sender: transientParent, signal: &QWindow::activeChanged, context: q, slot: &QWidget::hide); |
84 | |
85 | q->show(); |
86 | } |
87 | |
88 | void KToolTipWidgetPrivate::storeParent() |
89 | { |
90 | if (!content) { |
91 | return; |
92 | } |
93 | |
94 | contentParent = qobject_cast<QWidget *>(o: content->parent()); |
95 | } |
96 | |
97 | void KToolTipWidgetPrivate::restoreParent() |
98 | { |
99 | if (!content || !contentParent) { |
100 | return; |
101 | } |
102 | |
103 | content->setParent(contentParent); |
104 | } |
105 | |
106 | QPoint KToolTipWidgetPrivate::centerBelow(const QRect &rect, QScreen *screen) const |
107 | { |
108 | // It must be assured that: |
109 | // - the content is fully visible |
110 | // - the content is not drawn inside rect |
111 | |
112 | const QSize size = q->sizeHint(); |
113 | const int margin = q->style()->pixelMetric(metric: QStyle::PM_ToolTipLabelFrameWidth); |
114 | const QRect screenGeometry = screen->geometry(); |
115 | |
116 | const bool hasRoomToLeft = (rect.left() - size.width() - margin >= screenGeometry.left()); |
117 | const bool hasRoomToRight = (rect.right() + size.width() + margin <= screenGeometry.right()); |
118 | const bool hasRoomAbove = (rect.top() - size.height() - margin >= screenGeometry.top()); |
119 | const bool hasRoomBelow = (rect.bottom() + size.height() + margin <= screenGeometry.bottom()); |
120 | if (!hasRoomAbove && !hasRoomBelow && !hasRoomToLeft && !hasRoomToRight) { |
121 | return QPoint(); |
122 | } |
123 | |
124 | int x = 0; |
125 | int y = 0; |
126 | if (hasRoomBelow || hasRoomAbove) { |
127 | x = qMax(a: screenGeometry.left(), b: rect.center().x() - size.width() / 2); |
128 | if (x + size.width() >= screenGeometry.right()) { |
129 | x = screenGeometry.right() - size.width() + 1; |
130 | } |
131 | Q_ASSERT(x >= 0); |
132 | if (hasRoomBelow) { |
133 | y = rect.bottom() + margin; |
134 | } else { |
135 | y = rect.top() - size.height() - margin + 1; |
136 | } |
137 | } else { |
138 | Q_ASSERT(hasRoomToLeft || hasRoomToRight); |
139 | if (hasRoomToRight) { |
140 | x = rect.right() + margin; |
141 | } else { |
142 | x = rect.left() - size.width() - margin + 1; |
143 | } |
144 | // Put the tooltip at the bottom of the screen. The x-coordinate has already |
145 | // been adjusted, so that no overlapping with rect occurs. |
146 | y = screenGeometry.bottom() - size.height() + 1; |
147 | } |
148 | |
149 | return QPoint(x, y); |
150 | } |
151 | |
152 | KToolTipWidget::KToolTipWidget(QWidget *parent) |
153 | : QWidget(parent) |
154 | , d(new KToolTipWidgetPrivate(this)) |
155 | { |
156 | d->init(); |
157 | } |
158 | |
159 | KToolTipWidget::~KToolTipWidget() |
160 | { |
161 | d->restoreParent(); |
162 | } |
163 | |
164 | void KToolTipWidget::showAt(const QPoint &pos, QWidget *content, QWindow *transientParent) |
165 | { |
166 | d->addWidget(widget: content); |
167 | d->show(pos, transientParent); |
168 | } |
169 | |
170 | void KToolTipWidget::showBelow(const QRect &rect, QWidget *content, QWindow *transientParent) |
171 | { |
172 | d->addWidget(widget: content); |
173 | |
174 | const auto contentMargins = layout()->contentsMargins(); |
175 | const QSize screenSize = transientParent->screen()->geometry().size(); |
176 | |
177 | content->setMaximumSize(screenSize.shrunkBy(m: contentMargins)); |
178 | |
179 | d->show(pos: d->centerBelow(rect, screen: transientParent->screen()), transientParent); |
180 | } |
181 | |
182 | int KToolTipWidget::hideDelay() const |
183 | { |
184 | return d->hideTimer.interval(); |
185 | } |
186 | |
187 | void KToolTipWidget::hideLater() |
188 | { |
189 | if (!isVisible()) { |
190 | return; |
191 | } |
192 | |
193 | if (hideDelay() > 0) { |
194 | d->hideTimer.start(); |
195 | } else { |
196 | hide(); |
197 | } |
198 | } |
199 | |
200 | void KToolTipWidget::setHideDelay(int delay) |
201 | { |
202 | d->hideTimer.setInterval(delay); |
203 | } |
204 | |
205 | void KToolTipWidget::enterEvent(QEnterEvent *) |
206 | { |
207 | // Ignore hide delay and leave tooltip visible. |
208 | if (hideDelay() > 0) { |
209 | d->hideTimer.stop(); |
210 | } else { |
211 | hide(); |
212 | } |
213 | } |
214 | |
215 | void KToolTipWidget::hideEvent(QHideEvent *) |
216 | { |
217 | d->removeWidget(); |
218 | |
219 | QObject::disconnect(sender: windowHandle()->transientParent(), signal: &QWindow::activeChanged, receiver: this, slot: &QWidget::hide); |
220 | |
221 | // Give time to the content widget to get his own hide event. |
222 | QTimer::singleShot(interval: 0, receiver: this, slot: &KToolTipWidget::hidden); |
223 | } |
224 | |
225 | void KToolTipWidget::leaveEvent(QEvent *) |
226 | { |
227 | // Don't bother starting the hide timer, we are done. |
228 | hide(); |
229 | } |
230 | |
231 | void KToolTipWidget::paintEvent(QPaintEvent *event) |
232 | { |
233 | QStylePainter painter(this); |
234 | painter.setClipRegion(event->region()); |
235 | QStyleOptionFrame option; |
236 | option.initFrom(w: this); |
237 | painter.drawPrimitive(pe: QStyle::PE_PanelTipLabel, opt: option); |
238 | painter.end(); |
239 | |
240 | QWidget::paintEvent(event); |
241 | } |
242 | |
243 | #include "moc_ktooltipwidget.cpp" |
244 | |