Files
create/src/Gui/NotificationArea.cpp
Abdullah Tahiri ddd9a97211 NotificationArea: subscription to errors and warnings
=====================================================

Subscription to errors and warnings is controlled by parameters.
2023-03-26 11:33:57 +02:00

1084 lines
40 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 "PreCompiled.h"
#ifndef _PreComp_
#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>
#endif
#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 bp = boost::placeholders;
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
/** @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;
//@}
/** @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;
//@}
bool missedNotifications = false;
// Access control
std::mutex mutexNotification;
// Pointers to widgets (no ownership)
QMenu* menu;
QWidgetAction* notificationaction;
/** @name Resources */
//@{
/// Base::Console Message observer
std::unique_ptr<NotificationAreaObserver> observer;
Connection finishRestoreDocumentConnection;
/// Preference Parameter observer
std::unique_ptr<NotificationArea::ParameterObserver> parameterObserver;
//@}
};
}// namespace Gui
/******************* Resource Management *****************************************/
/** Simple class to manage Notification Area Resources*/
class ResourceManager
{
private:
ResourceManager()
{
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));
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;
/// 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) 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
bTranslatedNotification = true;// activate translated user notifications
}
NotificationAreaObserver::~NotificationAreaObserver()
{
Base::Console().DetachObserver(this);
}
void NotificationAreaObserver::SendLog(const std::string& notifiername, const std::string& msg,
Base::LogStyle level)
{
// 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.
auto simplifiedstring =
QString::fromStdString(msg)
.trimmed();// remove any leading and trailing whitespace character ('\n')
// avoid processing empty strings
if (simplifiedstring.isEmpty())
return;
if (level == Base::LogStyle::TranslatedNotification) {
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 msg;
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 QVariant();
}
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)
};
/** 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()
{
for (auto* item : pushedItems) {
if (item) {
delete item;
}
}
}
public:
/// deletes only notifications (messages of type Notification and TranslatedNotification)
void deleteNotifications()
{
if (tableWidget) {
for (int i = tableWidget->topLevelItemCount() - 1; i >= 0; i--) {
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
if (item->notificationType == Base::LogStyle::Notification
|| item->notificationType == Base::LogStyle::TranslatedNotification) {
delete item;
}
}
}
for (int i = pushedItems.size() - 1; i >= 0; i--) {
auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
if (item->notificationType == Base::LogStyle::Notification
|| item->notificationType == Base::LogStyle::TranslatedNotification) {
delete pushedItems.takeAt(i);
}
}
}
/// 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->unread;
});
}
/// returns the amount of notifications, errors and warnings currently being notified
inline int getCurrentlyNotifyingCount() const
{
return getCurrently([](auto* item) {
return item->notifying;
});
}
/// 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->shown;
});
}
/// 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
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
item->unread = false;
}
}
/// 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 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);
}
else {
delete tableWidget->topLevelItem(index - pushedItems.count());
}
}
/// 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);
}
/// pushes a notification item to the front
void push_front(NotificationItem* item)
{
pushedItems.push_front(item);
}
protected:
/// creates the Notifications Widget
QWidget* createWidget(QWidget* parent) override
{
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 item = tableWidget->itemAt(pos);
auto selectedItems = tableWidget->selectedItems();
QMenu menu;
QAction* del = menu.addAction(tr("Delete"), this, [&]() {
for (auto it : 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));
});
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++) {
auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
if (F(item)) {
instate++;
}
}
for (auto i = 0; i < pushedItems.count(); i++) {
auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
if (F(item)) {
instate++;
}
}
return instate;
}
private:
QTreeWidget* tableWidget;
// 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");
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;
}},
{"ErrorSubscriptionEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->observer->bErr = enabled;
}},
{"WarningSubscriptionEnabled",
[this](const std::string& string) {
auto enabled = hGrp->GetBool(string.c_str(), true);
notificationArea->pImp->observer->bWrn = enabled;
}},
};
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);
setMenu(pImp->menu);
auto na = new NotificationsAction(pImp->menu);
pImp->menu->addAction(na);
pImp->notificationaction = na;
// Signals for synchronisation of storage before showing/hiding the widget
QObject::connect(pImp->menu, &QMenu::aboutToHide, [&]() {
std::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]() {
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification list and indices
// while creating the tooltip
setText(QString::number(0)); // no unread notifications
if (pImp->missedNotifications) {
setIcon(TrayIcon::Normal);
pImp->missedNotifications = false;
}
static_cast<NotificationsAction*>(pImp->notificationaction)->synchroniseWidget();
// the position of the action has already been calculated (for a non-synchronised widget),
// the size could be recalculated here, but not the position according to the previous size.
// This resize event forces this recalculation.
QResizeEvent re(pImp->menu->size(), pImp->menu->size());
qApp->sendEvent(pImp->menu, &re);
});
// Connection to the finish restore signal to rearm Critical messages modal mode when action is
// user initiated
pImp->finishRestoreDocumentConnection =
App::GetApplication().signalFinishRestoreDocument.connect(
boost::bind(&Gui::NotificationArea::slotRestoreFinished, this, bp::_1));
// 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;
NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
QAction* delnotifications = menu.addAction(tr("Delete user notifications"), [&]() {
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification list and
// indices while creating the tooltip
na->deleteNotifications();
setText(QString::number(na->getUnreadCount()));
});
delnotifications->setEnabled(!na->isEmpty());
QAction* delall = menu.addAction(tr("Delete All"), [&]() {
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification list and
// indices while creating the tooltip
na->deleteAll();
setText(QString::number(0));
});
delall->setEnabled(!na->isEmpty());
menu.setDefaultAction(delall);
menu.exec(this->mapToGlobal(e->pos()));
}
QPushButton::mousePressEvent(e);
}
void NotificationArea::pushNotification(const QString& notifiername, const QString& message,
Base::LogStyle level)
{
auto* item = new NotificationItem(level, notifiername, message);
bool confirmation = confirmationRequired(level);
if (confirmation) {
showConfirmationDialog(notifiername, message);
}
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification list and indices while
// creating the tooltip
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();
}
na->push_front(item);
// If the non-intrusive notifications are disabled then stop here (messages added to the widget
// only)
if (pImp->notificationsDisabled) {
item->notifying =
false;// avoid mass of old notifications if feature is activated afterwards
setText(QString::number(
static_cast<NotificationsAction*>(pImp->notificationaction)->getUnreadCount()));
return;
}
// 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(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: ") + notifiername + QStringLiteral("\n\n") + message
+ QStringLiteral("\n\n")
+ QObject::tr("Do you want to 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()
{
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification list and indices while
// creating the tooltip
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;
while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->notifying) {
NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
if (item->shown) {
item->notifying = false;
item->shown = false;
}
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 =
QString::fromLatin1(
"<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"))
.arg(QObject::tr("Notifier"))
.arg(QObject::tr("Message"));
auto currentlynotifying = na->getCurrentlyNotifyingCount();
if (currentlynotifying > pImp->maxOpenNotifications) {
msgw +=
QString::fromLatin1(
" \
<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;
while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->notifying) {
if (i < pImp->maxOpenNotifications) {// show the first up to maxOpenNotifications
NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
QString iconstr;
if (item->notificationType == Base::LogStyle::Error) {
iconstr = QStringLiteral(":/icons/edit_Cancel.svg");
}
else if (item->notificationType == Base::LogStyle::Warning) {
iconstr = QStringLiteral(":/icons/Warning.svg");
}
else if (item->notificationType == Base::LogStyle::Critical) {
iconstr = QStringLiteral(":/icons/critical-info.svg");
}
else {
iconstr = QStringLiteral(":/icons/info.svg");
}
QString tmpmessage =
convertFromPlainText(item->msg, Qt::WhiteSpaceMode::WhiteSpaceNormal);
msgw +=
QString::fromLatin1(
" \
<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)
.arg(item->notifierName)
.arg(tmpmessage);
// start a timer for each of these notifications that was not previously shown
if (!item->shown) {
QTimer::singleShot(pImp->notificationExpirationTime, [this, item]() {
std::lock_guard<std::mutex> g(
pImp->mutexNotification);// guard to avoid modifying the notification
// start index while creating the tooltip
if (item) {
item->shown = false;
item->notifying = false;
if (pImp->autoRemoveUserNotifications) {
if (item->notificationType == Base::LogStyle::Notification
|| item->notificationType
== Base::LogStyle::TranslatedNotification) {
static_cast<NotificationsAction*>(pImp->notificationaction)
->deleteItem(item);
}
}
}
});
}
// We update the status to shown
item->shown = true;
}
else {// We do not have more space and older notifications will be too old
static_cast<NotificationItem*>(na->getItem(i))->notifying = false;
static_cast<NotificationItem*>(na->getItem(i))->shown = false;
}
i++;
}
msgw += QString::fromLatin1("</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(),
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());
}
}