Files
create/src/Gui/NotificationArea.cpp
Markus Reitböck a72a0d6405 Gui: use CMake to generate precompiled headers on all platforms
"Professional CMake" book suggest the following:

"Targets should build successfully with or without compiler support for precompiled headers. It
should be considered an optimization, not a requirement. In particular, do not explicitly include a
precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically
generated precompile header on the compiler command line instead. This is more portable across
the major compilers and is likely to be easier to maintain. It will also avoid warnings being
generated from certain code checking tools like iwyu (include what you use)."

Therefore, removed the "#include <PreCompiled.h>" from sources, also
there is no need for the "#ifdef _PreComp_" anymore
2025-09-14 09:47:03 +02:00

1277 lines
46 KiB
C++

/***************************************************************************
* Copyright (c) 2022 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 *
* *
***************************************************************************/
#include <QAction>
#include <QActionEvent>
#include <QApplication>
#include <QEvent>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMenu>
#include <QMessageBox>
#include <QStringList>
#include <QTextDocument>
#include <QThread>
#include <QTimer>
#include <QTreeWidget>
#include <QWidgetAction>
#include <memory>
#include <mutex>
#include <App/Application.h>
#include <Base/Console.h>
#include "Application.h"
#include "BitmapFactory.h"
#include "MDIView.h"
#include "MainWindow.h"
#include "NotificationBox.h"
#include "NotificationArea.h"
using namespace Gui;
using Connection = boost::signals2::connection;
namespace sp = std::placeholders;
using std::lock_guard;
class NotificationAreaObserver;
namespace Gui
{
/** PImpl idiom structure having all data necessary for the notification area */
struct NotificationAreaP
{
// Structure holding all variables necessary for the Notification Area.
// Preference parameters are updated by NotificationArea::ParameterObserver
//NOLINTBEGIN
/** @name Non-intrusive notifications parameters */
//@{
/// Parameter controlled
int maxOpenNotifications = 15;
/// Parameter controlled
unsigned int notificationExpirationTime = 10000;
/// minimum time that the notification will remain unclosed
unsigned int minimumOnScreenTime = 5000;
/// Parameter controlled
bool notificationsDisabled = false;
/// Control of confirmation mechanism (for Critical Messages)
bool requireConfirmationCriticalMessageDuringRestoring = true;
/// Width of the non-intrusive notification
int notificationWidth = 800;
/// Any open non-intrusive notifications will disappear when another window is activated
bool hideNonIntrusiveNotificationsWhenWindowDeactivated = true;
/// Prevent non-intrusive notifications from appearing when the FreeCAD Window is not the active
/// window
bool preventNonIntrusiveNotificationsWhenWindowNotActive = true;
//@}
/** @name Widget parameters */
//@{
/// Parameter controlled - maximum number of message allowed in the notification area widget (0
/// means no limit)
int maxWidgetMessages = 1000;
/// User notifications get automatically removed from the Widget after the non-intrusive
/// notification expiration time
bool autoRemoveUserNotifications = false;
//@}
/** @name Notification rate control */
//@{
/* Control rate of updates of non-intrusive messages (avoids visual artifacts when messages are
* constantly being received) */
// Timer to delay notification until a minimum time between two consecutive messages have lapsed
QTimer inhibitTimer;
// The time between two consecutive messages forced by the inhibitTimer
const unsigned int inhibitNotificationTime = 250;
//@}
/** @name Data source control */
//@{
/* Controls whether debug warnings and errors intended for developers should be processed or not
*/
bool developerErrorSubscriptionEnabled = false;
bool developerWarningSubscriptionEnabled = false;
//@}
bool missedNotifications = false;
// Access control
std::mutex mutexNotification;
// Pointers to widgets (no ownership)
QMenu* menu = nullptr;
QWidgetAction* notificationaction = nullptr;
/** @name Resources */
//@{
/// Base::Console Message observer
std::unique_ptr<NotificationAreaObserver> observer;
Connection finishRestoreDocumentConnection;
/// Preference Parameter observer
std::unique_ptr<NotificationArea::ParameterObserver> parameterObserver;
//@}
//NOLINTEND
};
}// namespace Gui
/******************* Resource Management *****************************************/
/** Simple class to manage Notification Area Resources*/
class ResourceManager
{
private:
ResourceManager()
{
//NOLINTBEGIN
error = BitmapFactory().pixmapFromSvg(":/icons/edit_Cancel.svg", QSize(16, 16));
warning = BitmapFactory().pixmapFromSvg(":/icons/Warning.svg", QSize(16, 16));
critical = BitmapFactory().pixmapFromSvg(":/icons/critical-info.svg", QSize(16, 16));
info = BitmapFactory().pixmapFromSvg(":/icons/info.svg", QSize(16, 16));
//NOLINTEND
notificationArea = QIcon(QStringLiteral(":/icons/InTray.svg"));
notificationAreaMissedNotifications =
QIcon(QStringLiteral(":/icons/InTray_missed_notifications.svg"));
}
inline static const auto& getResourceManager()
{
static ResourceManager manager;
return manager;
}
public:
inline static auto ErrorPixmap()
{
auto rm = getResourceManager();
return rm.error;
}
inline static auto WarningPixmap()
{
auto rm = getResourceManager();
return rm.warning;
}
inline static auto CriticalPixmap()
{
auto rm = getResourceManager();
return rm.critical;
}
inline static auto InfoPixmap()
{
auto rm = getResourceManager();
return rm.info;
}
inline static auto NotificationAreaIcon()
{
auto rm = getResourceManager();
return rm.notificationArea;
}
inline static auto notificationAreaMissedNotificationsIcon()
{
auto rm = getResourceManager();
return rm.notificationAreaMissedNotifications;
}
private:
QPixmap error;
QPixmap warning;
QPixmap critical;
QPixmap info;
QIcon notificationArea;
QIcon notificationAreaMissedNotifications;
};
/******************** Console Messages Observer (Console Interface) ************************/
/** This class listens to all messages sent via the console interface and
feeds the non-intrusive notification system and the notifications widget */
class NotificationAreaObserver: public Base::ILogger
{
public:
NotificationAreaObserver(NotificationArea* notificationarea);
~NotificationAreaObserver() override;
NotificationAreaObserver(const NotificationAreaObserver &) = delete;
NotificationAreaObserver(NotificationAreaObserver &&) = delete;
NotificationAreaObserver &operator=(const NotificationAreaObserver &) = delete;
NotificationAreaObserver &operator=(NotificationAreaObserver &&) = delete;
/// Function that is called by the console interface for this observer with the message
/// information
void sendLog(const std::string& notifiername, const std::string& msg, Base::LogStyle level,
Base::IntendedRecipient recipient, Base::ContentType content) override;
/// Name of the observer
const char* name() override
{
return "NotificationAreaObserver";
}
private:
NotificationArea* notificationArea;
};
NotificationAreaObserver::NotificationAreaObserver(NotificationArea* notificationarea)
: notificationArea(notificationarea)
{
Base::Console().attachObserver(this);
bLog = false; // ignore log messages
bMsg = false; // ignore messages
bNotification = true;// activate user notifications
}
NotificationAreaObserver::~NotificationAreaObserver()
{
Base::Console().detachObserver(this);
}
void NotificationAreaObserver::sendLog(const std::string& notifiername, const std::string& msg,
Base::LogStyle level, Base::IntendedRecipient recipient,
Base::ContentType content)
{
// 1. As notification system is shared with report view and others, the expectation is that any
// individual error and warning message will end in "\n". This means the string must be stripped
// of this character for representation in the notification area.
// 2. However, any message marked with the QT_TRANSLATE_NOOT macro with the "Notifications"
// context, shall not include
// "\n", as this generates problems with the translation system. Then the string must be
// stripped of "\n" before translation.
bool violatesBasicPolicy = (recipient == Base::IntendedRecipient::Developer
|| content == Base::ContentType::Untranslatable);
// We allow derogations for debug purposes according to user preferences
bool meetsDerogationCriteria = false;
if (violatesBasicPolicy) {
meetsDerogationCriteria =
(level == Base::LogStyle::Warning && notificationArea->areDeveloperWarningsActive())
|| (level == Base::LogStyle::Error && notificationArea->areDeveloperErrorsActive());
}
if (violatesBasicPolicy && !meetsDerogationCriteria) {
return;
}
auto simplifiedstring =
QString::fromStdString(msg)
.trimmed();// remove any leading and trailing whitespace character ('\n')
// avoid processing empty strings
if (simplifiedstring.isEmpty())
return;
if (content == Base::ContentType::Translated) {
notificationArea->pushNotification(
QString::fromStdString(notifiername), simplifiedstring, level);
}
else {
notificationArea->pushNotification(
QString::fromStdString(notifiername),
QCoreApplication::translate("Notifications", simplifiedstring.toUtf8()),
level);
}
}
/******************* Notification Widget *******************************************************/
/** Specialised Item class for the Widget notifications/errors/warnings
It holds all item specific data, including visualisation data and controls how
the item should appear in the widget.
*/
class NotificationItem: public QTreeWidgetItem
{
public:
NotificationItem(Base::LogStyle notificationtype, QString notifiername, QString message)
: notificationType(notificationtype),
notifierName(std::move(notifiername)),
msg(std::move(message))
{}
QVariant data(int column, int role) const override
{
// strings that will be displayed for each column of the widget
if (role == Qt::DisplayRole) {
switch (column) {
case 1:
return notifierName;
break;
case 2:
return getMessage();
break;
}
}
else if (column == 0 && role == Qt::DecorationRole) {
// Icons to be displayed for the first row
if (notificationType == Base::LogStyle::Error) {
return std::move(ResourceManager::ErrorPixmap());
}
else if (notificationType == Base::LogStyle::Warning) {
return std::move(ResourceManager::WarningPixmap());
}
else if (notificationType == Base::LogStyle::Critical) {
return std::move(ResourceManager::CriticalPixmap());
}
else {
return std::move(ResourceManager::InfoPixmap());
}
}
else if (role == Qt::FontRole) {
// Visualisation control of unread messages
static QFont font;
static QFont boldFont(font.family(), font.pointSize(), QFont::Bold);
if (unread) {
return boldFont;
}
return font;
}
return {};
}
void addRepetition() {
unread = true;
notifying = true;
shown = false;
repetitions++;
}
bool isRepeated(Base::LogStyle notificationtype, const QString & notifiername, const QString & message ) const {
return (notificationType == notificationtype && notifierName == notifiername && msg == message);
}
bool isType(Base::LogStyle notificationtype) const {
return notificationType == notificationtype;
}
bool isUnread() const {
return unread;
}
bool isNotifying() const {
return notifying;
}
bool isShown() const {
return shown;
}
int getRepetitions() const{
return repetitions;
}
void setNotified() {
notifying = false;
}
void resetNotified() {
notifying = true;
}
void setShown() {
shown = true;
}
void resetShown() {
shown = false;
}
void setRead() {
unread = false;
}
void setUnread() {
unread = true;
}
QString getMessage() const {
if(repetitions == 0) {
return msg;
}
else {
return msg + QObject::tr(" (%1 times)").arg(repetitions+1);
}
}
const QString & getNotifier() {
return notifierName;
}
private:
Base::LogStyle notificationType;
QString notifierName;
QString msg;
bool unread = true; // item is unread in the Notification Area Widget
bool notifying = true;// item is to be notified or being notified as non-intrusive message
bool shown = false; // item is already being notified (it is onScreen)
int repetitions = 0; // message appears n times in a row.
};
/** Drop menu Action containing the notifications widget.
* It stores all the notification item information in the form
* of NotificationItems (QTreeWidgetItem). This information is used
* by the Widget and by the non-intrusive messages.
* It owns the notification resources and is responsible for the release
* of the memory resources, either directly for the intermediate fast cache
* or indirectly via QT for the case of the QTreeWidgetItems.
*/
class NotificationsAction: public QWidgetAction
{
public:
NotificationsAction(QWidget* parent)
: QWidgetAction(parent)
{}
NotificationsAction(const NotificationsAction &) = delete;
NotificationsAction(NotificationsAction &&) = delete;
NotificationsAction & operator=(const NotificationsAction &) = delete;
NotificationsAction & operator=(NotificationsAction &&) = delete;
~NotificationsAction() override
{
for (auto* item : std::as_const(pushedItems)) {
if (item) {
delete item; // NOLINT
}
}
}
public:
/// deletes only notifications (messages of type Notification)
void deleteNotifications()
{
if (tableWidget) {
for (int i = tableWidget->topLevelItemCount() - 1; i >= 0; i--) {
//NOLINTNEXTLINE
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
if (item->isType(Base::LogStyle::Notification)) {
delete item; //NOLINT
}
}
}
for (int i = pushedItems.size() - 1; i >= 0; i--) {
//NOLINTNEXTLINE
auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
if (item->isType(Base::LogStyle::Notification)) {
delete pushedItems.takeAt(i); //NOLINT
}
}
}
/// deletes all notifications, errors and warnings
void deleteAll()
{
if (tableWidget) {
tableWidget->clear();// the parent will delete the items.
}
while (!pushedItems.isEmpty())
delete pushedItems.takeFirst();
}
/// returns the amount of unread notifications, errors and warnings
inline int getUnreadCount() const
{
return getCurrently([](auto* item) {
return item->isUnread();
});
}
/// returns the amount of notifications, errors and warnings currently being notified
inline int getCurrentlyNotifyingCount() const
{
return getCurrently([](auto* item) {
return item->isNotifying();
});
}
/// returns the amount of notifications, errors and warnings currently being shown as
/// non-intrusive messages (on-screen)
inline int getShownCount() const
{
return getCurrently([](auto* item) {
return item->isShown();
});
}
/// marks all notifications, errors and warnings as read
void clearUnreadFlag()
{
for (auto i = 0; i < tableWidget->topLevelItemCount();
i++) {// all messages were read, so clear the unread flag
//NOLINTNEXTLINE
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
item->setRead();
}
}
/// pushes all Notification Items to the Widget, so that they can be shown
void synchroniseWidget()
{
tableWidget->insertTopLevelItems(0, pushedItems);
pushedItems.clear();
}
/** pushes all Notification Items to the fast cache (this also prevents all unnecessary
* signaling from parents) and allows one to accelerate insertions and deletions
*/
void shiftToCache()
{
tableWidget->blockSignals(true);
tableWidget->clearSelection();
while (tableWidget->topLevelItemCount() > 0) {
auto* item = tableWidget->takeTopLevelItem(0);
pushedItems.push_back(item);
}
tableWidget->blockSignals(false);
}
/// returns if there are no notifications, errors and warnings at all
bool isEmpty() const
{
return tableWidget->topLevelItemCount() == 0 && pushedItems.isEmpty();
}
/// returns the total amount of notifications, errors and warnings currently stored
auto count() const
{
return tableWidget->topLevelItemCount() + pushedItems.count();
}
/// retrieves a pointer to a given notification from storage.
auto getItem(int index) const
{
if (index < pushedItems.count()) {
return pushedItems.at(index);
}
else {
return tableWidget->topLevelItem(index - pushedItems.count());
}
}
/// deletes a given notification, errors or warnings by index
void deleteItem(int index)
{
if (index < pushedItems.count()) {
delete pushedItems.takeAt(index); //NOLINT
}
else {
delete tableWidget->topLevelItem(index - pushedItems.count()); //NOLINT
}
}
/// deletes a given notification, errors or warnings by pointer
void deleteItem(NotificationItem* item)
{
for (int i = 0; i < count(); i++) {
if (getItem(i) == item) {
deleteItem(i);
return;
}
}
}
/// deletes the last Notification Item
void deleteLastItem()
{
deleteItem(count() - 1);
}
/// checks if last notification is the same
bool isSameNotification(const QString& notifiername, const QString& message,
Base::LogStyle level) const {
if(count() > 0) { // if not empty
//NOLINTNEXTLINE
auto item = static_cast<NotificationItem*>(getItem(0));
return item->isRepeated(level,notifiername,message);
}
return false;
}
void resetLastNotificationStatus() {
//NOLINTNEXTLINE
auto item = static_cast<NotificationItem*>(getItem(0));
item->addRepetition();
}
/// pushes a notification item to the front
auto push_front(std::unique_ptr<NotificationItem> item)
{
auto it = item.release();
pushedItems.push_front(it);
return it;
}
QSize size()
{
return tableWidget->size();
}
protected:
/// creates the Notifications Widget
QWidget* createWidget(QWidget* parent) override
{
//NOLINTBEGIN
QWidget* notificationsWidget = new QWidget(parent);
QHBoxLayout* layout = new QHBoxLayout(notificationsWidget);
notificationsWidget->setLayout(layout);
tableWidget = new QTreeWidget(parent);
tableWidget->setColumnCount(3);
QStringList headers;
headers << QObject::tr("Type") << QObject::tr("Notifier") << QObject::tr("Message");
tableWidget->setHeaderLabels(headers);
layout->addWidget(tableWidget);
tableWidget->setMaximumSize(1200, 600);
tableWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
tableWidget->header()->setStretchLastSection(false);
tableWidget->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
tableWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
// context menu on any item (row) of the widget
QObject::connect(
tableWidget, &QTreeWidget::customContextMenuRequested, [&](const QPoint& pos) {
auto selectedItems = tableWidget->selectedItems();
QMenu menu;
QAction* del = menu.addAction(tr("Delete"), this, [&]() {
for (auto it : std::as_const(selectedItems)) {
delete it;
}
});
del->setEnabled(!selectedItems.isEmpty());
menu.addSeparator();
QAction* delnotifications =
menu.addAction(tr("Delete User Notifications"),
this,
&NotificationsAction::deleteNotifications);
delnotifications->setEnabled(tableWidget->topLevelItemCount() > 0);
QAction* delall =
menu.addAction(tr("Delete All"), this, &NotificationsAction::deleteAll);
delall->setEnabled(tableWidget->topLevelItemCount() > 0);
menu.setDefaultAction(del);
menu.exec(tableWidget->mapToGlobal(pos));
});
//NOLINTEND
return notificationsWidget;
}
private:
/// utility function to return the number of Notification Items meeting the functor/lambda
/// criteria
int getCurrently(std::function<bool(const NotificationItem*)> F) const
{
int instate = 0;
for (auto i = 0; i < tableWidget->topLevelItemCount(); i++) {
//NOLINTNEXTLINE
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
if (F(item)) {
instate++;
}
}
for (auto i = 0; i < pushedItems.count(); i++) {
//NOLINTNEXTLINE
auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
if (F(item)) {
instate++;
}
}
return instate;
}
private:
QTreeWidget* tableWidget = nullptr;
// Intermediate storage
// Note: QTreeWidget is helplessly slow to single insertions, QTreeWidget is actually only
// necessary when showing the widget. A single QList insertion into a QTreeWidget is actually
// not that slow. The use of this intermediate storage substantially accelerates non-intrusive
// notifications.
QList<QTreeWidgetItem*> pushedItems;
};
/************ Parameter Observer (preferences) **************************************/
NotificationArea::ParameterObserver::ParameterObserver(NotificationArea* notificationarea)
: notificationArea(notificationarea)
{
hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/NotificationArea");
//NOLINTBEGIN
parameterMap = {
{"NotificationAreaEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
if (!enabled)
notificationArea->deleteLater();
}},
{"NonIntrusiveNotificationsEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->notificationsDisabled = !enabled;
}},
{"NotificationTime",
[this](const std::string& string) {
auto time = hGrp->GetInt(string.c_str(), 20) * 1000;
if (time < 0)
time = 0;
notificationArea->pImp->notificationExpirationTime = static_cast<unsigned int>(time);
}},
{"MinimumOnScreenTime",
[this](const std::string& string) {
auto time = hGrp->GetInt(string.c_str(), 5) * 1000;
if (time < 0)
time = 0;
notificationArea->pImp->minimumOnScreenTime = static_cast<unsigned int>(time);
}},
{"MaxOpenNotifications",
[this](const std::string& string) {
auto limit = hGrp->GetInt(string.c_str(), 15);
if (limit < 0)
limit = 0;
notificationArea->pImp->maxOpenNotifications = static_cast<unsigned int>(limit);
}},
{"MaxWidgetMessages",
[this](const std::string& string) {
auto limit = hGrp->GetInt(string.c_str(), 1000);
if (limit < 0)
limit = 0;
notificationArea->pImp->maxWidgetMessages = static_cast<unsigned int>(limit);
}},
{"AutoRemoveUserNotifications",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->autoRemoveUserNotifications = enabled;
}},
{"NotificiationWidth",
[this](const std::string& string) {
auto width = hGrp->GetInt(string.c_str(), 800);
if (width < 300)
width = 300;
notificationArea->pImp->notificationWidth = width;
}},
{"HideNonIntrusiveNotificationsWhenWindowDeactivated",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->hideNonIntrusiveNotificationsWhenWindowDeactivated = enabled;
}},
{"PreventNonIntrusiveNotificationsWhenWindowNotActive",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->preventNonIntrusiveNotificationsWhenWindowNotActive = enabled;
}},
{"DeveloperErrorSubscriptionEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), false);
notificationArea->pImp->developerErrorSubscriptionEnabled = enabled;
}},
{"DeveloperWarningSubscriptionEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), false);
notificationArea->pImp->developerWarningSubscriptionEnabled = enabled;
}},
};
//NOLINTEND
for (auto& val : parameterMap) {
auto string = val.first;
auto update = val.second;
update(string);
}
hGrp->Attach(this);
}
NotificationArea::ParameterObserver::~ParameterObserver()
{
hGrp->Detach(this);
}
void NotificationArea::ParameterObserver::OnChange(Base::Subject<const char*>& rCaller,
const char* sReason)
{
(void)rCaller;
auto key = parameterMap.find(sReason);
if (key != parameterMap.end()) {
auto string = key->first;
auto update = key->second;
update(string);
}
}
/************************* Notification Area *****************************************/
NotificationArea::NotificationArea(QWidget* parent)
: QPushButton(parent)
{
// QPushButton appearance
setText(QString());
setFlat(true);
// Initialisation of pImpl structure
pImp = std::make_unique<NotificationAreaP>();
pImp->observer = std::make_unique<NotificationAreaObserver>(this);
pImp->parameterObserver = std::make_unique<NotificationArea::ParameterObserver>(this);
pImp->menu = new QMenu(parent); //NOLINT
setMenu(pImp->menu);
auto na = new NotificationsAction(pImp->menu); //NOLINT
pImp->menu->addAction(na);
pImp->notificationaction = na;
//NOLINTBEGIN
// Signals for synchronisation of storage before showing/hiding the widget
QObject::connect(pImp->menu, &QMenu::aboutToHide, [&]() {
lock_guard<std::mutex> g(pImp->mutexNotification);
static_cast<NotificationsAction*>(pImp->notificationaction)->clearUnreadFlag();
static_cast<NotificationsAction*>(pImp->notificationaction)->shiftToCache();
});
QObject::connect(pImp->menu, &QMenu::aboutToShow, [this]() {
// guard to avoid modifying the notification list and indices while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
setText(QString::number(0)); // no unread notifications
if (pImp->missedNotifications) {
setIcon(TrayIcon::Normal);
pImp->missedNotifications = false;
}
static_cast<NotificationsAction*>(pImp->notificationaction)->synchroniseWidget();
// There is a Qt bug in not respecting a QMenu size when size changes in aboutToShow.
//
// https://bugreports.qt.io/browse/QTBUG-54421
// https://forum.qt.io/topic/68765/how-to-update-geometry-on-qaction-visibility-change-in-qmenu-abouttoshow/3
//
// None of this works
// pImp->menu->updateGeometry();
// pImp->menu->adjustSize();
// pImp->menu->ensurePolished();
// this->updateGeometry();
// this->adjustSize();
// this->ensurePolished();
// This does correct the size
QSize size = static_cast<NotificationsAction*>(pImp->notificationaction)->size();
QResizeEvent re(size, size);
qApp->sendEvent(pImp->menu, &re);
// This corrects the position of the menu
QTimer::singleShot(0, [&] {
QWidget* statusbar = static_cast<QWidget*>(this->parent());
QPoint statusbar_top_right = statusbar->mapToGlobal(statusbar->rect().topRight());
QSize menusize = pImp->menu->size();
QWidget* w = this;
QPoint button_pos = w->mapToGlobal(w->rect().topLeft());
QPoint widget_pos;
if ((statusbar_top_right.x() - menusize.width()) > button_pos.x()) {
widget_pos = QPoint(button_pos.x(), statusbar_top_right.y() - menusize.height());
}
else {
widget_pos = QPoint(statusbar_top_right.x() - menusize.width(),
statusbar_top_right.y() - menusize.height());
}
pImp->menu->move(widget_pos);
});
});
// Connection to the finish restore signal to rearm Critical messages modal mode when action is
// user initiated
pImp->finishRestoreDocumentConnection =
App::GetApplication().signalFinishRestoreDocument.connect(
std::bind(&Gui::NotificationArea::slotRestoreFinished, this, sp::_1));
//NOLINTEND
// Initialisation of the timer to inhibit continuous updates of the notification system in case
// clusters of messages arrive (so as to delay the actual notification until the whole cluster
// has arrived)
pImp->inhibitTimer.setSingleShot(true);
pImp->inhibitTimer.callOnTimeout([this, na]() {
setText(QString::number(na->getUnreadCount()));
showInNotificationArea();
});
setIcon(TrayIcon::Normal);
}
NotificationArea::~NotificationArea()
{
pImp->finishRestoreDocumentConnection.disconnect();
}
void NotificationArea::mousePressEvent(QMouseEvent* e)
{
// Contextual menu (e.g. to delete Notifications or all messages (Notifications, Errors,
// Warnings and Critical messages)
if (e->button() == Qt::RightButton && hitButton(e->pos())) {
QMenu menu;
//NOLINTBEGIN
NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
QAction* delnotifications = menu.addAction(tr("Delete User Notifications"), [&]() {
// guard to avoid modifying the notification list and indices while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
na->deleteNotifications();
setText(QString::number(na->getUnreadCount()));
});
delnotifications->setEnabled(!na->isEmpty());
QAction* delall = menu.addAction(tr("Delete All"), [&]() {
// guard to avoid modifying the notification list and indices while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
na->deleteAll();
setText(QString::number(0));
});
//NOLINTEND
delall->setEnabled(!na->isEmpty());
menu.setDefaultAction(delall);
menu.exec(this->mapToGlobal(e->pos()));
}
QPushButton::mousePressEvent(e);
}
bool NotificationArea::areDeveloperWarningsActive() const
{
return pImp->developerWarningSubscriptionEnabled;
}
bool NotificationArea::areDeveloperErrorsActive() const
{
return pImp->developerErrorSubscriptionEnabled;
}
void NotificationArea::pushNotification(const QString& notifiername, const QString& message,
Base::LogStyle level)
{
auto confirmation = confirmationRequired(level);
if (confirmation) {
showConfirmationDialog(notifiername, message);
}
// guard to avoid modifying the notification list and indices while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
//NOLINTNEXTLINE
NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
// Limit the maximum number of messages stored in the widget (0 means no limit)
if (pImp->maxWidgetMessages != 0 && na->count() > pImp->maxWidgetMessages) {
na->deleteLastItem();
}
auto repeated = na->isSameNotification(notifiername, message, level);
if(!repeated) {
auto itemptr = std::make_unique<NotificationItem>(level, notifiername, message);
auto item = na->push_front(std::move(itemptr));
// If the non-intrusive notifications are disabled then stop here (messages added to the widget
// only)
if (pImp->notificationsDisabled) {
item->setNotified(); // avoid mass of old notifications if feature is activated afterwards
//NOLINTBEGIN
setText(QString::number(
static_cast<NotificationsAction*>(pImp->notificationaction)->getUnreadCount()));
return;
//NOLINTEND
}
}
else {
na->resetLastNotificationStatus();
}
// start or restart rate control (the timer is rearmed if not yet expired, expiration triggers
// showing of the notification messages)
//
// NOTE: The inhibition timer is created in the main thread and cannot be restarted from another
// QThread. A QTimer can be moved to another QThread (from the QThread in which it is). However,
// it can only be create in a QThread, not in any other thread.
//
// For this reason, the timer is only triggered if this QThread is the QTimer thread.
//
// But I want my message from my thread to appear in the notification area. Fine, then configure
// Console not to use the direct connection mode, but the Queued one:
// Base::Console().setConnectionMode(ConnectionMode::Queued);
auto timer_thread = pImp->inhibitTimer.thread();
auto current_thread = QThread::currentThread();
if (timer_thread == current_thread) {
pImp->inhibitTimer.start(static_cast<int>(pImp->inhibitNotificationTime));
}
}
bool NotificationArea::confirmationRequired(Base::LogStyle level)
{
auto userInitiatedRestore =
Application::Instance->testStatus(Gui::Application::UserInitiatedOpenDocument);
return (level == Base::LogStyle::Critical && userInitiatedRestore
&& pImp->requireConfirmationCriticalMessageDuringRestoring);
}
void NotificationArea::showConfirmationDialog(const QString& notifiername, const QString& message)
{
auto confirmMsg = QObject::tr("Notifier:") + QStringLiteral(" ") + notifiername + QStringLiteral("\n\n") + message
+ QStringLiteral("\n\n")
+ QObject::tr("Skip confirmation of further critical message notifications "
"while loading the file?");
auto button = QMessageBox::critical(getMainWindow()->activeWindow(),
QObject::tr("Critical message"),
confirmMsg,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (button == QMessageBox::Yes)
pImp->requireConfirmationCriticalMessageDuringRestoring = false;
}
void NotificationArea::showInNotificationArea()
{
// guard to avoid modifying the notification list and indices while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
//NOLINTNEXTLINE
NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
if (!NotificationBox::isVisible()) {
// The Notification Box may have been closed (by the user by popping it out or by left mouse
// button) ensure that old notifications are not shown again, even if the timer has not
// lapsed
int i = 0;
//NOLINTNEXTLINE
while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->isNotifying()) {
//NOLINTNEXTLINE
NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
if (item->isShown()) {
item->setNotified();
item->resetShown();
}
i++;
}
}
auto currentlyshown = na->getShownCount();
// If we cannot show more messages, we do no need to update the non-intrusive notification
if (currentlyshown < pImp->maxOpenNotifications) {
// There is space for at least one more notification
// We update the message with the most recent up to maxOpenNotifications
QString msgw =
QStringLiteral(
"<style>p { margin: 0 0 0 0 } td { padding: 0 15px }</style> \
<p style='white-space:normal'> \
<table> \
<tr> \
<th><small>%1</small></th> \
<th><small>%2</small></th> \
<th><small>%3</small></th> \
</tr>")
.arg(QObject::tr("Type"), QObject::tr("Notifier"), QObject::tr("Message"));
auto currentlynotifying = na->getCurrentlyNotifyingCount();
if (currentlynotifying > pImp->maxOpenNotifications) {
msgw +=
QStringLiteral(
" \
<tr> \
<td align='left'><img width=\"16\" height=\"16\" src=':/icons/Warning.svg'></td> \
<td align='left'>FreeCAD</td> \
<td align='left'>%1</td> \
</tr>")
.arg(QObject::tr("Too many opened non-intrusive notifications. Notifications "
"are being omitted!"));
}
int i = 0;
//NOLINTNEXTLINE
while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->isNotifying()) {
if (i < pImp->maxOpenNotifications) {// show the first up to maxOpenNotifications
//NOLINTNEXTLINE
NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
QString iconstr;
if (item->isType(Base::LogStyle::Error)) {
iconstr = QStringLiteral(":/icons/edit_Cancel.svg");
}
else if (item->isType(Base::LogStyle::Warning)) {
iconstr = QStringLiteral(":/icons/Warning.svg");
}
else if (item->isType(Base::LogStyle::Critical)) {
iconstr = QStringLiteral(":/icons/critical-info.svg");
}
else {
iconstr = QStringLiteral(":/icons/info.svg");
}
QString tmpmessage =
convertFromPlainText(item->getMessage(), Qt::WhiteSpaceMode::WhiteSpaceNormal);
msgw +=
QStringLiteral(
" \
<tr> \
<td align='left'><img width=\"16\" height=\"16\" src='%1'></td> \
<td align='left'>%2</td> \
<td align='left'>%3</td> \
</tr>")
.arg(iconstr, item->getNotifier(), tmpmessage);
// start a timer for each of these notifications that was not previously shown
if (!item->isShown()) {
QTimer::singleShot(pImp->notificationExpirationTime, [this, item, repetitions = item->getRepetitions()]() {
// guard to avoid modifying the notification
// start index while creating the tooltip
lock_guard<std::mutex> g(pImp->mutexNotification);
// if the item exists and the number of repetitions has not changed in the
// meantime
if (item && item->getRepetitions() == repetitions) {
item->resetShown();
item->setNotified();
if (pImp->autoRemoveUserNotifications) {
if (item->isType(Base::LogStyle::Notification)) {
//NOLINTNEXTLINE
static_cast<NotificationsAction*>(pImp->notificationaction)
->deleteItem(item);
}
}
}
});
}
// We update the status to shown
item->setShown();
}
else {// We do not have more space and older notifications will be too old
//NOLINTBEGIN
static_cast<NotificationItem*>(na->getItem(i))->setNotified();
static_cast<NotificationItem*>(na->getItem(i))->resetShown();
//NOLINTEND
}
i++;
}
msgw += QStringLiteral("</table></p>");
NotificationBox::Options options = NotificationBox::Options::RestrictAreaToReference;
if (pImp->preventNonIntrusiveNotificationsWhenWindowNotActive) {
options = options | NotificationBox::Options::OnlyIfReferenceActive;
}
if (pImp->hideNonIntrusiveNotificationsWhenWindowDeactivated) {
options = options | NotificationBox::Options::HideIfReferenceWidgetDeactivated;
}
bool isshown = NotificationBox::showText(this->mapToGlobal(QPoint()),
msgw,
getMainWindow(),
static_cast<int>(pImp->notificationExpirationTime),
pImp->minimumOnScreenTime,
options,
pImp->notificationWidth);
if (!isshown && !pImp->missedNotifications) {
pImp->missedNotifications = true;
setIcon(TrayIcon::MissedNotifications);
}
}
}
void NotificationArea::slotRestoreFinished(const App::Document&)
{
// Re-arm on restore critical message modal notifications if another document is loaded
pImp->requireConfirmationCriticalMessageDuringRestoring = true;
}
void NotificationArea::setIcon(TrayIcon trayIcon)
{
if (trayIcon == TrayIcon::Normal) {
QPushButton::setIcon(ResourceManager::NotificationAreaIcon());
}
else if (trayIcon == TrayIcon::MissedNotifications) {
QPushButton::setIcon(ResourceManager::notificationAreaMissedNotificationsIcon());
}
}