Gui: NotificationBox - New non-intrusive auto-closing notification box

======================================================================

In short, a "tooltip" alike notification, where the user can continue working without having
to interact with the notification. If the user is interested in the notification, he or she can
stop to read it. If not interested, the user can ignore it and continue working. The notification
will automatically disappear when the timer lapses or before that time, as described below.

A new widget similar to QToolTip, to have a similar look and feel
and interface, while avoiding early closing on user action.

QToolTip is not intended for notifications, but to provide contextual help. While
QToolTip could have been used for part of the functionality (by filtering out events),
other parts required additional changes to the interface

Gui::NotificationBox is a reimplementation intended to provide user notifications. It
relies on the proven code of QToolTip for the wanted behaviour.

Additional functionality:
- A notification box has a minimum time for which it won't close, unless popped out (click inside
the notification).
- After the minimum time, if left mouse button is clicked (anywhere) it auto-closes, as it is
understood the user has continued working.
- After a maximum time, it will automatically close (even in nothing is clicked).
This commit is contained in:
Abdullah Tahiri
2023-02-02 14:26:50 +01:00
committed by abdullahtahiriyo
parent 6d4a90c605
commit 8925399dbb
3 changed files with 429 additions and 0 deletions

View File

@@ -996,6 +996,7 @@ SET(Widget_CPP_SRCS
FileDialog.cpp
MainWindow.cpp
MainWindowPy.cpp
NotificationBox.cpp
PrefWidgets.cpp
InputField.cpp
ProgressBar.cpp
@@ -1014,6 +1015,7 @@ SET(Widget_HPP_SRCS
FileDialog.h
MainWindow.h
MainWindowPy.h
NotificationBox.h
PrefWidgets.h
InputField.h
ProgressBar.h

352
src/Gui/NotificationBox.cpp Normal file
View File

@@ -0,0 +1,352 @@
/***************************************************************************
* Copyright (c) 2023 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#ifndef _PreComp_
# include <memory>
# include <mutex>
# include <QApplication>
# include <QAction>
# include <QActionEvent>
# include <QDesktopWidget>
# include <QEvent>
# include <QHBoxLayout>
# include <QHeaderView>
# include <QLabel>
# include <QMenu>
# include <QPointer>
# include <QScreen>
# include <QStyleOption>
# include <QStylePainter>
# include <QStringList>
# include <QTextDocument>
# include <QTimer>
#endif
#include "NotificationBox.h"
using namespace Gui;
namespace Gui {
/** Class showing the notification as a label
* */
class NotificationLabel : public QLabel
{
Q_OBJECT
public:
// Windows implementation uses QWidget w to pass the screen (see NotificationBox::showText).
// This screen is used as parent for QLabel.
// Linux implementation does not rely on a parent (w = nullptr).
NotificationLabel(const QString &text, const QPoint &pos, QWidget *w, int msecDisplayTime, int minShowTime = 0);
~NotificationLabel();
/// Reuse existing notification to show a new notification (with a new text)
void reuseNotification(const QString &text, int msecDisplayTime, const QPoint &pos);
/// Hide notification after a hiding timer.
void hideNotification();
/// Updates the size of the QLabel
void updateSize(const QPoint &pos);
/// Event filter
bool eventFilter(QObject *, QEvent *) override;
/// Returns true if the text provided is the same as the one of an existing notification
bool notificationLabelChanged(const QString &text);
/// Place the notification at the given position
void placeNotificationLabel(const QPoint &pos);
/// The instance
static NotificationLabel *instance;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
/// Re-start the notification expiration timer
void restartExpireTimer(int msecDisplayTime);
/// Hide notification right away
void hideNotificationImmediately();
private:
int minShowTime;
QTimer hideTimer;
QTimer expireTimer;
};
NotificationLabel *NotificationLabel::instance = nullptr;
NotificationLabel::NotificationLabel(const QString &text, const QPoint &pos, QWidget *w, int msecDisplayTime, int minShowTime)
: QLabel(w, Qt::ToolTip | Qt::BypassGraphicsProxyWidget), minShowTime(minShowTime)
{
delete instance;
instance = this;
setForegroundRole(QPalette::ToolTipText); // defaults to ToolTip QPalette
setBackgroundRole(QPalette::ToolTipBase); // defaults to ToolTip QPalette
setPalette(NotificationBox::palette());
ensurePolished();
setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this));
setFrameStyle(QFrame::NoFrame);
setAlignment(Qt::AlignLeft);
setIndent(1);
qApp->installEventFilter(this);
setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, nullptr, this) / 255.0);
setMouseTracking(false);
hideTimer.setSingleShot(true);
expireTimer.setSingleShot(true);
expireTimer.callOnTimeout([this](){
hideTimer.stop();
expireTimer.stop();
hideNotificationImmediately();
});
hideTimer.callOnTimeout([this](){
expireTimer.stop();
hideNotificationImmediately();
});
reuseNotification(text, msecDisplayTime, pos);
}
void NotificationLabel::restartExpireTimer(int msecDisplayTime)
{
int time = 10000 + 40 * qMax(0, text().length()-100);
if (msecDisplayTime > 0) {
time = msecDisplayTime;
}
expireTimer.start(time);
hideTimer.stop();
}
void NotificationLabel::reuseNotification(const QString &text, int msecDisplayTime, const QPoint &pos)
{
setText(text);
updateSize(pos);
restartExpireTimer(msecDisplayTime);
}
void NotificationLabel::updateSize(const QPoint &pos)
{
// Ensure that we get correct sizeHints by placing this window on the right screen.
QFontMetrics fm(font());
QSize extra(1, 0);
// Make it look good with the default ToolTip font on Mac, which has a small descent.
if (fm.descent() == 2 && fm.ascent() >= 11) {
++extra.rheight();
}
setWordWrap(Qt::mightBeRichText(text()));
QSize sh = sizeHint();
// ### When the above WinRT code is fixed, windowhandle should be used to find the screen.
QScreen *screen = QGuiApplication::screenAt(pos);
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
if (screen) {
const qreal screenWidth = screen->geometry().width();
if (!wordWrap() && sh.width() > screenWidth) {
setWordWrap(true);
sh = sizeHint();
}
}
resize(sh + extra);
}
void NotificationLabel::paintEvent(QPaintEvent *ev)
{
QStylePainter p(this);
QStyleOptionFrame opt;
opt.init(this);
p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
p.end();
QLabel::paintEvent(ev);
}
void NotificationLabel::resizeEvent(QResizeEvent *e)
{
QStyleHintReturnMask frameMask;
QStyleOption option;
option.init(this);
if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask)) {
setMask(frameMask.region);
}
QLabel::resizeEvent(e);
}
NotificationLabel::~NotificationLabel()
{
instance = nullptr;
}
void NotificationLabel::hideNotification()
{
if (!hideTimer.isActive()) {
hideTimer.start(300);
}
}
void NotificationLabel::hideNotificationImmediately()
{
close(); // to trigger QEvent::Close which stops the animation
deleteLater();
}
bool NotificationLabel::eventFilter(QObject *o, QEvent *e)
{
Q_UNUSED(o)
switch (e->type()) {
case QEvent::MouseButtonPress:
{
// If minimum on screen time has already lapsed - hide the notification no matter where the click was done
auto total = expireTimer.interval();
auto remaining = expireTimer.remainingTime();
auto lapsed = total - remaining;
// ... or if the click is inside the notification, hide it no matter if the minimum onscreen time has lapsed or not
if( lapsed > minShowTime || this->underMouse()) {
hideNotification();
return false;
}
}
default:
break;
}
return false;
}
void NotificationLabel::placeNotificationLabel(const QPoint &pos)
{
QPoint p = pos;
const QScreen *screen = QGuiApplication::screenAt(pos);
// a QScreen's handle *should* never be null, so this is a bit paranoid
if (screen && screen->handle()) {
const QSize cursorSize = QSize(16, 16);
QPoint offset(2, cursorSize.height());
// assuming an arrow shape, we can just move to the side for very large cursors
if (cursorSize.height() > 2 * this->height())
offset = QPoint(cursorSize.width() / 2, 0);
p += offset;
QRect screenRect = screen->geometry();
if (p.x() + this->width() > screenRect.x() + screenRect.width())
p.rx() -= 4 + this->width();
if (p.y() + this->height() > screenRect.y() + screenRect.height())
p.ry() -= 24 + this->height();
if (p.y() < screenRect.y())
p.setY(screenRect.y());
if (p.x() + this->width() > screenRect.x() + screenRect.width())
p.setX(screenRect.x() + screenRect.width() - this->width());
if (p.x() < screenRect.x())
p.setX(screenRect.x());
if (p.y() + this->height() > screenRect.y() + screenRect.height())
p.setY(screenRect.y() + screenRect.height() - this->height());
}
this->move(p);
}
bool NotificationLabel::notificationLabelChanged(const QString &text)
{
if (NotificationLabel::instance->text() != text) {
return true;
}
return false;
}
/***************************** NotificationBox **********************************/
void NotificationBox::showText(const QPoint &pos, const QString &text, int msecDisplayTime, unsigned int minShowTime)
{
// a label does already exist
if (NotificationLabel::instance && NotificationLabel::instance->isVisible()){
if (text.isEmpty()){ // empty text means hide current label
NotificationLabel::instance->hideNotification();
return;
}
else {
// If the label has changed, reuse the one that is showing (removes flickering)
if (NotificationLabel::instance->notificationLabelChanged(text)){
NotificationLabel::instance->reuseNotification(text, msecDisplayTime, pos);
NotificationLabel::instance->placeNotificationLabel(pos);
}
return;
}
}
// no label can be reused, create new label:
if (!text.isEmpty()){
#ifdef Q_OS_WIN32
// On windows, we can't use the widget as parent otherwise the window will be
// raised when the toollabel will be shown
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
new NotificationLabel(text, pos, QGuiApplication::screenAt(pos), msecDisplayTime, minShowTime); // NotificationLabel manages its own lifetime.
QT_WARNING_POP
#else
new NotificationLabel(text, pos, nullptr, msecDisplayTime, minShowTime); // sets NotificationLabel::instance to itself
#endif
NotificationLabel::instance->placeNotificationLabel(pos);
NotificationLabel::instance->setObjectName(QLatin1String("NotificationBox_label"));
NotificationLabel::instance->showNormal();
}
}
bool NotificationBox::isVisible()
{
return (NotificationLabel::instance != nullptr && NotificationLabel::instance->isVisible());
}
QString NotificationBox::text()
{
if (NotificationLabel::instance)
return NotificationLabel::instance->text();
return QString();
}
Q_GLOBAL_STATIC(QPalette, notificationbox_palette)
QPalette NotificationBox::palette()
{
return *notificationbox_palette();
}
QFont NotificationBox::font()
{
return QApplication::font("NotificationLabel");
}
void NotificationBox::setPalette(const QPalette &palette)
{
*notificationbox_palette() = palette;
if (NotificationLabel::instance)
NotificationLabel::instance->setPalette(palette);
}
void NotificationBox::setFont(const QFont &font)
{
QApplication::setFont(font, "NotificationLabel");
}
} // namespace Gui
#include "NotificationBox.moc"

75
src/Gui/NotificationBox.h Normal file
View File

@@ -0,0 +1,75 @@
/***************************************************************************
* Copyright (c) 2023 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com > *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#ifndef GUI_NOTIFICATIONBOX_H
#define GUI_NOTIFICATIONBOX_H
namespace Gui {
/** This class provides a non-intrusive tip alike notification
* dialog, which unlike QToolTip, is kept shown during a time.
*
* The notification is shown during minShowTime, unless pop out
* (i.e. clicked inside the notification).
*
* The notification will show up to a maximum of msecShowTime. The
* only event that closes the notification between minShowTime and
* msecShowTime is a mouse button click (anywhere of the screen).
*
* When msecShowTime is not provided, it is calculated based on the length
* of the text.
*
* This class interface and its implementation are based on QT's
* QToolTip.
*/
class NotificationBox
{
NotificationBox() = delete;
public:
/** Shows a non-intrusive notification.
* @param pos Position at which the notification will be shown
* @param msecShowTime Time after which the notification will auto-close (unless it is closed by an event, see class documentation above)
* @param minShowTime Time during which the notification can only be made disappear by popping it out (clicking inside it).
*/
static void showText(const QPoint &pos, const QString &text, int msecShowTime = -1, unsigned int minShowTime = 0);
/// Hides a notification.
static inline void hideText() { showText(QPoint(), QString()); }
/// Returns whether a notification is being shown or not.
static bool isVisible();
/// Returns the text of the notification.
static QString text();
/// Returns the palette.
static QPalette palette();
/// Sets the palette.
static void setPalette(const QPalette &);
/// Returns the font of the notification.
static QFont font();
/// Sets the font to be used in the notification.
static void setFont(const QFont &);
};
} // namespace Gui
#endif // GUI_NOTIFICATIONBOX_H