1
/***************************************************************************
2
* Copyright (c) 2023 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************/
23
#include "PreCompiled.h"
25
#include <QApplication>
29
#include <QStyleOption>
30
#include <QStylePainter>
31
#include <QTextDocument>
37
#include "NotificationBox.h"
45
// https://stackoverflow.com/questions/41402152/stdunique-ptr-and-qobjectdeletelater
46
struct QObjectDeleteLater
48
void operator()(QObject* o)
55
using qobject_delete_later_unique_ptr = std::unique_ptr<T, QObjectDeleteLater>;
57
/** Class showing the notification as a label
59
class NotificationLabel: public QLabel
63
NotificationLabel(const QString& text, const QPoint& pos, int displayTime, int minShowTime = 0,
65
/// Reuse existing notification to show a new notification (with a new text)
66
void reuseNotification(const QString& text, int displayTime, const QPoint& pos, int width);
67
/// Hide notification after a hiding timer.
68
void hideNotification();
69
/// Update the size of the QLabel
70
void updateSize(const QPoint& pos);
72
bool eventFilter(QObject*, QEvent*) override;
73
/// Return true if the text provided is the same as the one of an existing notification
74
bool notificationLabelChanged(const QString& text);
75
/// Place the notification at the given position
76
void placeNotificationLabel(const QPoint& pos);
77
/// Set the windowrect defining an area to which the label should be constrained
78
void setTipRect(const QRect& restrictionarea);
80
void setHideIfReferenceWidgetDeactivated(bool on);
83
static qobject_delete_later_unique_ptr<NotificationLabel> instance;
86
void paintEvent(QPaintEvent* e) override;
87
void resizeEvent(QResizeEvent* e) override;
90
/// Re-start the notification expiration timer
91
void restartExpireTimer(int displayTime);
92
/// Hide notification right away
93
void hideNotificationImmediately();
100
QRect restrictionArea;
101
bool hideIfReferenceWidgetDeactivated;
104
qobject_delete_later_unique_ptr<NotificationLabel> NotificationLabel::instance = nullptr;
106
NotificationLabel::NotificationLabel(const QString& text, const QPoint& pos, int displayTime,
107
int minShowTime, int width)
108
: QLabel(nullptr, Qt::ToolTip | Qt::BypassGraphicsProxyWidget),
109
minShowTime(minShowTime)
111
instance.reset(this);
112
setForegroundRole(QPalette::ToolTipText);// defaults to ToolTip QPalette
113
setBackgroundRole(QPalette::ToolTipBase);// defaults to ToolTip QPalette
114
setPalette(NotificationBox::palette());
116
setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this));
117
setFrameStyle(QFrame::NoFrame);
118
setAlignment(Qt::AlignLeft);
120
qApp->installEventFilter(this);
121
setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, nullptr, this) / 255.0);
122
setMouseTracking(false);
123
hideTimer.setSingleShot(true);
124
expireTimer.setSingleShot(true);
126
expireTimer.callOnTimeout([this]() {
128
hideNotificationImmediately();
131
hideTimer.callOnTimeout([this]() {
133
hideNotificationImmediately();
136
reuseNotification(text, displayTime, pos, width);
139
void NotificationLabel::restartExpireTimer(int displayTime)
143
if (displayTime > 0) {
147
time = 10000 + 40 * qMax(0, text().length() - 100);
150
expireTimer.start(time);
154
void NotificationLabel::reuseNotification(const QString& text, int displayTime, const QPoint& pos,
158
setFixedWidth(width);
162
restartExpireTimer(displayTime);
165
void NotificationLabel::updateSize(const QPoint& pos)
167
// Ensure that we get correct sizeHints by placing this window on the right screen.
168
QFontMetrics fm(font());
170
// Make it look good with the default ToolTip font on Mac, which has a small descent.
171
if (fm.descent() == 2 && fm.ascent() >= 11) {
175
setWordWrap(Qt::mightBeRichText(text()));
177
QSize sh = sizeHint();
179
// ### When the above WinRT code is fixed, windowhandle should be used to find the screen.
180
QScreen* screen = QGuiApplication::screenAt(pos);
183
screen = QGuiApplication::primaryScreen();
187
const qreal screenWidth = screen->geometry().width();
188
if (!wordWrap() && sh.width() > screenWidth) {
197
void NotificationLabel::paintEvent(QPaintEvent* ev)
199
QStylePainter p(this);
200
QStyleOptionFrame opt;
202
p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
204
QLabel::paintEvent(ev);
207
void NotificationLabel::resizeEvent(QResizeEvent* e)
209
QStyleHintReturnMask frameMask;
212
option.initFrom(this);
214
if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask)) {
215
setMask(frameMask.region);
218
QLabel::resizeEvent(e);
221
void NotificationLabel::hideNotification()
223
if (!hideTimer.isActive()) {
224
hideTimer.start(300);
228
void NotificationLabel::hideNotificationImmediately()
230
close();// to trigger QEvent::Close which stops the animation
234
bool NotificationLabel::eventFilter(QObject* o, QEvent* e)
239
case QEvent::MouseButtonPress: {
240
// If minimum on screen time has already lapsed - hide the notification no matter where
241
// the click was done
242
auto total = expireTimer.interval();
243
auto remaining = expireTimer.remainingTime();
244
auto lapsed = total - remaining;
245
// ... or if the click is inside the notification, hide it no matter if the minimum
246
// onscreen time has lapsed or not
247
auto insideclick = this->underMouse();
248
if (lapsed > minShowTime || insideclick) {
254
case QEvent::WindowDeactivate:
255
if (hideIfReferenceWidgetDeactivated)
256
hideNotificationImmediately();
265
void NotificationLabel::placeNotificationLabel(const QPoint& pos)
268
const QScreen* screen = QGuiApplication::screenAt(pos);
269
// a QScreen's handle *should* never be null, so this is a bit paranoid
270
if (screen && screen->handle()) {
271
const QSize cursorSize = QSize(16, 16);
273
QPoint offset(2, cursorSize.height());
274
// assuming an arrow shape, we can just move to the side for very large cursors
275
if (cursorSize.height() > 2 * this->height())
276
offset = QPoint(cursorSize.width() / 2, 0);
280
QRect actinglimit = screen->geometry();
282
if (!restrictionArea.isNull())
283
actinglimit = restrictionArea;
285
const int standard_x_padding = 4;
286
const int standard_y_padding = 24;
288
if (p.x() + this->width() > actinglimit.x() + actinglimit.width())
289
p.rx() -= standard_x_padding + this->width();
290
if (p.y() + standard_y_padding + this->height() > actinglimit.y() + actinglimit.height())
291
p.ry() -= standard_y_padding + this->height();
292
if (p.y() < actinglimit.y())
293
p.setY(actinglimit.y());
294
if (p.x() + this->width() > actinglimit.x() + actinglimit.width())
295
p.setX(actinglimit.x() + actinglimit.width() - this->width());
296
if (p.x() < actinglimit.x())
297
p.setX(actinglimit.x());
298
if (p.y() + this->height() > actinglimit.y() + actinglimit.height())
299
p.setY(actinglimit.y() + actinglimit.height() - this->height());
305
void NotificationLabel::setTipRect(const QRect& restrictionarea)
307
restrictionArea = restrictionarea;
310
void NotificationLabel::setHideIfReferenceWidgetDeactivated(bool on)
312
hideIfReferenceWidgetDeactivated = on;
315
bool NotificationLabel::notificationLabelChanged(const QString& text)
317
return NotificationLabel::instance->text() != text;
320
/***************************** NotificationBox **********************************/
322
bool NotificationBox::showText(const QPoint& pos, const QString& text, QWidget* referenceWidget,
323
int displayTime, unsigned int minShowTime, Options options,
326
QRect restrictionarea = {};
328
if (referenceWidget) {
329
if (options & Options::OnlyIfReferenceActive) {
330
if (!referenceWidget->isActiveWindow()) {
335
if (options & Options::RestrictAreaToReference) {
336
// Calculate the main window QRect in global screen coordinates.
337
auto mainwindowrect = referenceWidget->rect();
339
restrictionarea = QRect(referenceWidget->mapToGlobal(mainwindowrect.topLeft()),
340
mainwindowrect.size());
344
// a label does already exist
345
if (NotificationLabel::instance && NotificationLabel::instance->isVisible()) {
346
if (text.isEmpty()) {// empty text means hide current label
347
NotificationLabel::instance->hideNotification();
351
// If the label has changed, reuse the one that is showing (removes flickering)
352
if (NotificationLabel::instance->notificationLabelChanged(text)) {
353
NotificationLabel::instance->setTipRect(restrictionarea);
354
NotificationLabel::instance->setHideIfReferenceWidgetDeactivated(
355
options & Options::HideIfReferenceWidgetDeactivated);
356
NotificationLabel::instance->reuseNotification(text, displayTime, pos, width);
357
NotificationLabel::instance->placeNotificationLabel(pos);
363
// no label can be reused, create new label:
364
if (!text.isEmpty()) {
365
// Note: The Label takes no parent, as on windows, we can't use the widget as parent
366
// otherwise the window will be raised when the tooltip will be shown. We do not use
367
// it on Linux either for consistency.
368
new NotificationLabel(text,
372
width);// sets NotificationLabel::instance to itself
374
NotificationLabel::instance->setTipRect(restrictionarea);
375
NotificationLabel::instance->setHideIfReferenceWidgetDeactivated(
376
options & Options::HideIfReferenceWidgetDeactivated);
377
NotificationLabel::instance->placeNotificationLabel(pos);
378
NotificationLabel::instance->setObjectName(QLatin1String("NotificationBox_label"));
380
NotificationLabel::instance->showNormal();
386
bool NotificationBox::isVisible()
388
return (NotificationLabel::instance != nullptr && NotificationLabel::instance->isVisible());
391
QString NotificationBox::text()
393
if (NotificationLabel::instance)
394
return NotificationLabel::instance->text();
398
Q_GLOBAL_STATIC(QPalette, notificationbox_palette)
400
QPalette NotificationBox::palette()
402
return *notificationbox_palette();
405
QFont NotificationBox::font()
407
return QApplication::font("NotificationLabel");
410
void NotificationBox::setPalette(const QPalette& palette)
412
*notificationbox_palette() = palette;
413
if (NotificationLabel::instance)
414
NotificationLabel::instance->setPalette(palette);
417
void NotificationBox::setFont(const QFont& font)
419
QApplication::setFont(font, "NotificationLabel");
424
#include "NotificationBox.moc"