Gui: Handle user notifications

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

This commit provides specific behaviour on how to handle user messages signalled by App::Document for the different types of notifications.

For critical messages DURING a user initiated restore (i.e. only if the user clicked in the UI, not applicable for macros or Python initiated):
- The first critical message during restore requires user confirmation by clicking a button in a modal dialog box. The user has the choice to
request to be asked for confirmation for any subsequent critical message during restore or to forgo confirmation.
- If he chooses to forgo confirmation, subsequent critical messages are shown as auto-closing non-modal dialogs in a non-intrusive way. The user
can continue working while this information is shown. There is a maximum of auto-closing non-modal dialgos that can be enqueued. Over this maximum,
A warning pop up indicates this situation, referring to the Report View. No further auto-closing messages are enqueued until all existing messages
have disappeared. This is done to prevent overwhelming the user with notifications in cases where malfunction causes too many notifications to be
generated.

For any type of message OUTSIDE a user initiated restore:
- Messages are shown as non-intrusive non-modal auto-closing messages (it is not possible to generate modal/blocking messages outside a user
initiated restore).
- Messages are enqueued respecting a limit as above.
This commit is contained in:
Abdullah Tahiri
2022-12-18 09:32:15 +01:00
committed by abdullahtahiriyo
parent 8b36a2780b
commit bcc65d2fbb

View File

@@ -28,6 +28,7 @@
# include <QFileInfo>
# include <QMessageBox>
# include <QTextStream>
# include <QTimer>
# include <Inventor/actions/SoSearchAction.h>
# include <Inventor/nodes/SoSeparator.h>
#endif
@@ -70,6 +71,175 @@ namespace bp = boost::placeholders;
namespace Gui {
/** This class is an implementation only class to handle user notifications offered by App::Document.
*
* It provides a mechanism requiring confirmation for critical notifications only during User initiated restore/document loading ( it
* does not require confirmation for macro/Python initiated restore, not to interfere with automations).
*
* Additionally, it provides a mechanism to show autoclosing non-modal user notifications in a non-intrusive way.
**/
class MessageManager {
public:
MessageManager() = default;
~MessageManager();
void setDocument(Gui::Document * pDocument);
void slotUserMessage(const App::DocumentObject&, const QString &, App::Document::NotificationType);
private:
void reorderAutoClosingMessages();
QMessageBox* createNonModalMessage(const QString & msg, App::Document::NotificationType notificationtype);
void pushAutoClosingMessage(const QString & msg, App::Document::NotificationType notificationtype);
void pushAutoClosingMessageTooManyMessages();
private:
using Connection = boost::signals2::connection;
Gui::Document * pDoc;
Connection connectUserMessage;
bool requireConfirmationCriticalMessageDuringRestoring = true;
std::vector<QMessageBox*> openAutoClosingMessages;
std::mutex mutexAutoClosingMessages;
const int autoClosingTimeout = 5000; // ms
const int autoClosingMessageStackingOffset = 10;
const unsigned int maxNumberOfOpenAutoClosingMessages = 3;
bool maxNumberOfOpenAutoClosingMessagesLimitReached = false;
};
MessageManager::~MessageManager(){
connectUserMessage.disconnect();
}
void MessageManager::setDocument(Gui::Document * pDocument)
{
pDoc = pDocument;
connectUserMessage = pDoc->getDocument()->signalUserMessage.connect
(boost::bind(&Gui::MessageManager::slotUserMessage, this, bp::_1, bp::_2, bp::_3));
}
void MessageManager::slotUserMessage(const App::DocumentObject& obj, const QString & msg, App::Document::NotificationType notificationtype)
{
(void) obj;
auto userInitiatedRestore = Application::Instance->testStatus(Gui::Application::UserInitiatedOpenDocument);
if(notificationtype == App::Document::NotificationType::Critical && userInitiatedRestore && requireConfirmationCriticalMessageDuringRestoring) {
auto confirmMsg = msg + QStringLiteral("\n\n") + QObject::tr("Do you want to skip confirmation of further critical message notifications while loading the file?");
auto button = QMessageBox::critical(pDoc->getActiveView(), QObject::tr("Critical Message"), confirmMsg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
if(button == QMessageBox::Yes)
requireConfirmationCriticalMessageDuringRestoring = false;
}
else { // Non-critical errors and warnings - auto-closing non-blocking message box
auto messageNumber = openAutoClosingMessages.size();
// Not opening more than the number of maximum autoclosing messages
// If maximum reached, the mechanism only resets after all present messages are auto-closed
if( messageNumber < maxNumberOfOpenAutoClosingMessages) {
if(messageNumber == 0 && maxNumberOfOpenAutoClosingMessagesLimitReached) {
maxNumberOfOpenAutoClosingMessagesLimitReached = false;
}
if(!maxNumberOfOpenAutoClosingMessagesLimitReached) {
pushAutoClosingMessage(msg, notificationtype);
}
}
else {
if(!maxNumberOfOpenAutoClosingMessagesLimitReached)
pushAutoClosingMessageTooManyMessages();
maxNumberOfOpenAutoClosingMessagesLimitReached = true;
}
}
}
void MessageManager::pushAutoClosingMessage(const QString & msg, App::Document::NotificationType notificationtype)
{
std::lock_guard<std::mutex> g(mutexAutoClosingMessages); // guard to avoid creating new messages while closing old messages (via timer)
auto msgBox = createNonModalMessage(msg, notificationtype);
msgBox->show();
int numberOpenAutoClosingMessages = openAutoClosingMessages.size();
openAutoClosingMessages.push_back(msgBox);
reorderAutoClosingMessages();
QTimer::singleShot(autoClosingTimeout*numberOpenAutoClosingMessages, [msgBox, this](){
std::lock_guard<std::mutex> g(mutexAutoClosingMessages); // guard to avoid closing old messages while creating new ones
if(msgBox) {
msgBox->done(0);
openAutoClosingMessages.erase(
std::remove(openAutoClosingMessages.begin(), openAutoClosingMessages.end(), msgBox),
openAutoClosingMessages.end());
reorderAutoClosingMessages();
}
});
}
void MessageManager::pushAutoClosingMessageTooManyMessages()
{
pushAutoClosingMessage(QObject::tr("Too many message notifications. Notification temporarily stopped. Look at the report view for more information."), App::Document::NotificationType::Warning);
}
QMessageBox* MessageManager::createNonModalMessage(const QString & msg, App::Document::NotificationType notificationtype)
{
auto parent = pDoc->getActiveView();
QMessageBox* msgBox = new QMessageBox(parent);
msgBox->setAttribute(Qt::WA_DeleteOnClose); // msgbox deleted automatically upon closed
msgBox->setStandardButtons(QMessageBox::NoButton);
msgBox->setWindowFlag(Qt::FramelessWindowHint,true);
msgBox->setText(msg);
if(notificationtype == App::Document::NotificationType::Error) {
msgBox->setWindowTitle(QObject::tr("Error"));
msgBox->setIcon(QMessageBox::Critical);
}
else if(notificationtype == App::Document::NotificationType::Warning) {
msgBox->setWindowTitle(QObject::tr("Warning"));
msgBox->setIcon(QMessageBox::Warning);
}
else if(notificationtype == App::Document::NotificationType::Information) {
msgBox->setWindowTitle(QObject::tr("Information"));
msgBox->setIcon(QMessageBox::Information);
}
else if(notificationtype == App::Document::NotificationType::Critical) {
msgBox->setWindowTitle(QObject::tr("Critical"));
msgBox->setIcon(QMessageBox::Critical);
}
msgBox->setModal( false ); // if you want it non-modal
return msgBox;
}
void MessageManager::reorderAutoClosingMessages()
{
auto parent = pDoc->getActiveView();
int numberOpenAutoClosingMessages = openAutoClosingMessages.size();
auto x = parent->width() / 2;
auto y = parent->height() / 7;
int posindex = numberOpenAutoClosingMessages - 1;
for (auto rit = openAutoClosingMessages.rbegin(); rit != openAutoClosingMessages.rend(); ++rit, posindex--) {
int xw = x - (*rit)->width() / 2 + autoClosingMessageStackingOffset*posindex;;
int yw = y + autoClosingMessageStackingOffset*posindex;
(*rit)->move(xw, yw);
(*rit)->raise();
}
}
// Pimpl class
struct DocumentP
{
@@ -130,8 +300,9 @@ struct DocumentP
using ConnectionBlock = boost::signals2::shared_connection_block;
ConnectionBlock connectActObjectBlocker;
ConnectionBlock connectChangeDocumentBlocker;
};
MessageManager messageManager;
};
} // namespace Gui
/* TRANSLATOR Gui::Document */
@@ -211,6 +382,8 @@ Document::Document(App::Document* pcDocument,Application * app)
(boost::bind(&Gui::Document::slotTransactionAppend, this, bp::_1, bp::_2));
d->connectTransactionRemove = pcDocument->signalTransactionRemove.connect
(boost::bind(&Gui::Document::slotTransactionRemove, this, bp::_1, bp::_2));
d->messageManager.setDocument(this);
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
@@ -1074,7 +1247,7 @@ static bool checkCanonicalPath(const std::map<App::Document*, bool> &docs)
<< QObject::tr("Path:") << ' ' << QString::fromUtf8(doc->FileName.getValue());
for (auto d : v.second) {
if (d == doc) continue;
ts << "\n"
ts << "\n"
<< QObject::tr("Document:") << ' ' << docName(d)
<< "\n "
<< QObject::tr("Path:") << ' ' << QString::fromUtf8(d->FileName.getValue());
@@ -1690,7 +1863,6 @@ void Document::slotFinishImportObjects(const std::vector<App::DocumentObject*> &
// }
}
void Document::addRootObjectsToGroup(const std::vector<App::DocumentObject*>& obj, App::DocumentObjectGroup* grp)
{
std::map<App::DocumentObject*, bool> rootMap;