diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 30addf3efa..1a1630508b 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -999,6 +999,7 @@ SET(Widget_CPP_SRCS MainWindow.cpp MainWindowPy.cpp NotificationBox.cpp + NotificationArea.cpp PrefWidgets.cpp InputField.cpp ProgressBar.cpp @@ -1018,6 +1019,7 @@ SET(Widget_HPP_SRCS MainWindow.h MainWindowPy.h NotificationBox.h + NotificationArea.h PrefWidgets.h InputField.h ProgressBar.h diff --git a/src/Gui/NotificationArea.cpp b/src/Gui/NotificationArea.cpp new file mode 100644 index 0000000000..84d943e6f2 --- /dev/null +++ b/src/Gui/NotificationArea.cpp @@ -0,0 +1,993 @@ +/*************************************************************************** + * Copyright (c) 2022 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 * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include +#include + +#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; + //@} + + /** @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; + //@} + + // Access control + std::mutex mutexNotification; + + // Pointers to widgets (no ownership) + QMenu* menu; + QWidgetAction* notificationaction; + + /** @name Resources */ + //@{ + /// Base::Console Message observer + std::unique_ptr observer; + Connection finishRestoreDocumentConnection; + /// Preference Parameter observer + std::unique_ptr 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)); + } + + 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; + } + +private: + QPixmap error; + QPixmap warning; + QPixmap critical; + QPixmap info; +}; + +/******************** 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(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(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(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 F) const + { + int instate = 0; + for (auto i = 0; i < tableWidget->topLevelItemCount(); i++) { + auto* item = static_cast(tableWidget->topLevelItem(i)); + if (F(item)) { + instate++; + } + } + for (auto i = 0; i < pushedItems.count(); i++) { + auto* item = static_cast(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 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(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(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(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(limit); + }}, + {"AutoRemoveUserNotifications", + [this](const std::string& string) { + auto enabled = hGrp->GetBool(string.c_str(), true); + notificationArea->pImp->autoRemoveUserNotifications = 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& 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(); + + pImp->observer = std::make_unique(this); + pImp->parameterObserver = std::make_unique(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 g(pImp->mutexNotification); + static_cast(pImp->notificationaction)->clearUnreadFlag(); + static_cast(pImp->notificationaction)->shiftToCache(); + }); + + QObject::connect(pImp->menu, &QMenu::aboutToShow, [this]() { + std::lock_guard g( + pImp->mutexNotification);// guard to avoid modifying the notification list and indices + // while creating the tooltip + setText(QString::number(0)); // no unread notifications + static_cast(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(); + }); +} + +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(pImp->notificationaction); + + QAction* delnotifications = menu.addAction(tr("Delete user notifications"), [&]() { + std::lock_guard 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 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 g( + pImp->mutexNotification);// guard to avoid modifying the notification list and indices while + // creating the tooltip + + NotificationsAction* na = static_cast(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(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 g( + pImp->mutexNotification);// guard to avoid modifying the notification list and indices while + // creating the tooltip + + NotificationsAction* na = static_cast(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(na->getItem(i))->notifying) { + NotificationItem* item = static_cast(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( + " \ +

\ + \ + \ + \ + \ + \ + ") + .arg(QObject::tr("Type")) + .arg(QObject::tr("Notifier")) + .arg(QObject::tr("Message")); + + auto currentlynotifying = na->getCurrentlyNotifyingCount(); + + if (currentlynotifying > pImp->maxOpenNotifications) { + msgw += + QString::fromLatin1( + " \ + \ + \ + \ + \ + ") + .arg(QObject::tr("Too many opened non-intrusive notifications. Notifications " + "are being omitted!")); + } + + int i = 0; + + while (i < na->count() && static_cast(na->getItem(i))->notifying) { + + if (i < pImp->maxOpenNotifications) {// show the first up to maxOpenNotifications + NotificationItem* item = static_cast(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"); + } + + msgw += + QString::fromLatin1( + " \ + \ + \ + \ + \ + ") + .arg(iconstr) + .arg(item->notifierName) + .arg(item->msg); + + // 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 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(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(na->getItem(i))->notifying = false; + static_cast(na->getItem(i))->shown = false; + } + + i++; + } + + msgw += QString::fromLatin1("
%1%2%3
FreeCAD%1
%2%3

"); + + + NotificationBox::showText(this->mapToGlobal(QPoint()), + msgw, + pImp->notificationExpirationTime, + pImp->minimumOnScreenTime); + } +} + +void NotificationArea::slotRestoreFinished(const App::Document&) +{ + // Re-arm on restore critical message modal notifications if another document is loaded + pImp->requireConfirmationCriticalMessageDuringRestoring = true; +} diff --git a/src/Gui/NotificationArea.h b/src/Gui/NotificationArea.h new file mode 100644 index 0000000000..133cb41737 --- /dev/null +++ b/src/Gui/NotificationArea.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright (c) 2022 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_NOTIFICATIONAREA_H +#define GUI_NOTIFICATIONAREA_H + +#include +#include + +#include +#include + +#include +#include + +namespace Gui +{ + +struct NotificationAreaP; + +class NotificationArea: public QPushButton +{ +public: + class ParameterObserver: public ParameterGrp::ObserverType + { + public: + explicit ParameterObserver(NotificationArea* notificationarea); + ~ParameterObserver(); + + void OnChange(Base::Subject& rCaller, const char* sReason) override; + + private: + NotificationArea* notificationArea; + ParameterGrp::handle hGrp; + std::map> parameterMap; + }; + + NotificationArea(QWidget* parent = nullptr); + ~NotificationArea(); + + void pushNotification(const QString& notifiername, const QString& message, + Base::LogStyle level); + +private: + void showInNotificationArea(); + bool confirmationRequired(Base::LogStyle level); + void showConfirmationDialog(const QString& notifiername, const QString& message); + void slotRestoreFinished(const App::Document&); + + void mousePressEvent(QMouseEvent* e) override; + +private: + std::unique_ptr pImp; +}; + + +}// namespace Gui + +#endif// GUI_NOTIFICATIONAREA_H