diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 0a19ba4bf4..b60dc4849e 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -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 diff --git a/src/Gui/NotificationBox.cpp b/src/Gui/NotificationBox.cpp new file mode 100644 index 0000000000..0fce38bd1a --- /dev/null +++ b/src/Gui/NotificationBox.cpp @@ -0,0 +1,352 @@ +/*************************************************************************** + * Copyright (c) 2023 Abdullah Tahiri * + * * + * 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 +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#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" + diff --git a/src/Gui/NotificationBox.h b/src/Gui/NotificationBox.h new file mode 100644 index 0000000000..2fb22b3b14 --- /dev/null +++ b/src/Gui/NotificationBox.h @@ -0,0 +1,75 @@ +/*************************************************************************** + * Copyright (c) 2023 Abdullah Tahiri * + * * + * 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