FreeCAD

Форк
0
/
NotificationArea.cpp 
1277 строк · 45.7 Кб
1
/***************************************************************************
2
 *   Copyright (c) 2022 Abdullah Tahiri <abdullah.tahiri.yo@gmail.com>     *
3
 *                                                                         *
4
 *   This file is part of the FreeCAD CAx development system.              *
5
 *                                                                         *
6
 *   This library is free software; you can redistribute it and/or         *
7
 *   modify it under the terms of the GNU Library General Public           *
8
 *   License as published by the Free Software Foundation; either          *
9
 *   version 2 of the License, or (at your option) any later version.      *
10
 *                                                                         *
11
 *   This library  is distributed in the hope that it will be useful,      *
12
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14
 *   GNU Library General Public License for more details.                  *
15
 *                                                                         *
16
 *   You should have received a copy of the GNU Library General Public     *
17
 *   License along with this library; see the file COPYING.LIB. If not,    *
18
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
19
 *   Suite 330, Boston, MA  02111-1307, USA                                *
20
 *                                                                         *
21
 ***************************************************************************/
22

23
#include "PreCompiled.h"
24

25
#ifndef _PreComp_
26
#include <QAction>
27
#include <QActionEvent>
28
#include <QApplication>
29
#include <QEvent>
30
#include <QHBoxLayout>
31
#include <QHeaderView>
32
#include <QMenu>
33
#include <QMessageBox>
34
#include <QStringList>
35
#include <QTextDocument>
36
#include <QThread>
37
#include <QTimer>
38
#include <QTreeWidget>
39
#include <QWidgetAction>
40
#include <memory>
41
#include <mutex>
42
#endif
43

44
#include <App/Application.h>
45
#include <Base/Console.h>
46

47
#include "Application.h"
48
#include "BitmapFactory.h"
49
#include "MDIView.h"
50
#include "MainWindow.h"
51
#include "NotificationBox.h"
52

53
#include "NotificationArea.h"
54

55
using namespace Gui;
56

57
using Connection = boost::signals2::connection;
58

59
namespace sp = std::placeholders;
60

61
using std::lock_guard;
62

63
class NotificationAreaObserver;
64

65
namespace Gui
66
{
67
/** PImpl idiom structure having all data necessary for the notification area */
68
struct NotificationAreaP
69
{
70
    // Structure holding all variables necessary for the Notification Area.
71
    // Preference parameters are updated by NotificationArea::ParameterObserver
72

73
    //NOLINTBEGIN
74
    /** @name Non-intrusive notifications parameters */
75
    //@{
76
    /// Parameter controlled
77
    int maxOpenNotifications = 15;
78
    /// Parameter controlled
79
    unsigned int notificationExpirationTime = 10000;
80
    /// minimum time that the notification will remain unclosed
81
    unsigned int minimumOnScreenTime = 5000;
82
    /// Parameter controlled
83
    bool notificationsDisabled = false;
84
    /// Control of confirmation mechanism (for Critical Messages)
85
    bool requireConfirmationCriticalMessageDuringRestoring = true;
86
    /// Width of the non-intrusive notification
87
    int notificationWidth = 800;
88
    /// Any open non-intrusive notifications will disappear when another window is activated
89
    bool hideNonIntrusiveNotificationsWhenWindowDeactivated = true;
90
    /// Prevent non-intrusive notifications from appearing when the FreeCAD Window is not the active
91
    /// window
92
    bool preventNonIntrusiveNotificationsWhenWindowNotActive = true;
93
    //@}
94

95
    /** @name Widget parameters */
96
    //@{
97
    /// Parameter controlled - maximum number of message allowed in the notification area widget (0
98
    /// means no limit)
99
    int maxWidgetMessages = 1000;
100
    /// User notifications get automatically removed from the Widget after the non-intrusive
101
    /// notification expiration time
102
    bool autoRemoveUserNotifications = false;
103
    //@}
104

105
    /** @name Notification rate control */
106
    //@{
107
    /* Control rate of updates of non-intrusive messages (avoids visual artifacts when messages are
108
     * constantly being received) */
109
    // Timer to delay notification until a minimum time between two consecutive messages have lapsed
110
    QTimer inhibitTimer;
111
    // The time between two consecutive messages forced by the inhibitTimer
112
    const unsigned int inhibitNotificationTime = 250;
113
    //@}
114

115
    /** @name Data source control */
116
    //@{
117
    /* Controls whether debug warnings and errors intended for developers should be processed or not
118
     */
119
    bool developerErrorSubscriptionEnabled = false;
120
    bool developerWarningSubscriptionEnabled = false;
121
    //@}
122

123
    bool missedNotifications = false;
124

125
    // Access control
126
    std::mutex mutexNotification;
127

128
    // Pointers to widgets (no ownership)
129
    QMenu* menu = nullptr;
130
    QWidgetAction* notificationaction = nullptr;
131

132
    /** @name Resources */
133
    //@{
134
    /// Base::Console Message observer
135
    std::unique_ptr<NotificationAreaObserver> observer;
136
    Connection finishRestoreDocumentConnection;
137
    /// Preference Parameter observer
138
    std::unique_ptr<NotificationArea::ParameterObserver> parameterObserver;
139
    //@}
140

141
    //NOLINTEND
142
};
143

144
}// namespace Gui
145

146

147
/******************* Resource Management *****************************************/
148

149
/** Simple class to manage Notification Area Resources*/
150
class ResourceManager
151
{
152

153
private:
154
    ResourceManager()
155
    {
156
        //NOLINTBEGIN
157
        error = BitmapFactory().pixmapFromSvg(":/icons/edit_Cancel.svg", QSize(16, 16));
158
        warning = BitmapFactory().pixmapFromSvg(":/icons/Warning.svg", QSize(16, 16));
159
        critical = BitmapFactory().pixmapFromSvg(":/icons/critical-info.svg", QSize(16, 16));
160
        info = BitmapFactory().pixmapFromSvg(":/icons/info.svg", QSize(16, 16));
161
        //NOLINTEND
162
        notificationArea = QIcon(QStringLiteral(":/icons/InTray.svg"));
163
        notificationAreaMissedNotifications =
164
            QIcon(QStringLiteral(":/icons/InTray_missed_notifications.svg"));
165
    }
166

167
    inline static const auto& getResourceManager()
168
    {
169
        static ResourceManager manager;
170

171
        return manager;
172
    }
173

174
public:
175
    inline static auto ErrorPixmap()
176
    {
177
        auto rm = getResourceManager();
178
        return rm.error;
179
    }
180

181
    inline static auto WarningPixmap()
182
    {
183
        auto rm = getResourceManager();
184
        return rm.warning;
185
    }
186

187
    inline static auto CriticalPixmap()
188
    {
189
        auto rm = getResourceManager();
190
        return rm.critical;
191
    }
192

193
    inline static auto InfoPixmap()
194
    {
195
        auto rm = getResourceManager();
196
        return rm.info;
197
    }
198

199
    inline static auto NotificationAreaIcon()
200
    {
201
        auto rm = getResourceManager();
202
        return rm.notificationArea;
203
    }
204

205
    inline static auto notificationAreaMissedNotificationsIcon()
206
    {
207
        auto rm = getResourceManager();
208
        return rm.notificationAreaMissedNotifications;
209
    }
210

211
private:
212
    QPixmap error;
213
    QPixmap warning;
214
    QPixmap critical;
215
    QPixmap info;
216
    QIcon notificationArea;
217
    QIcon notificationAreaMissedNotifications;
218
};
219

220
/******************** Console Messages Observer (Console Interface) ************************/
221

222
/** This class listens to all messages sent via the console interface and
223
   feeds the non-intrusive notification system and the notifications widget */
224
class NotificationAreaObserver: public Base::ILogger
225
{
226
public:
227
    NotificationAreaObserver(NotificationArea* notificationarea);
228
    ~NotificationAreaObserver() override;
229

230
    NotificationAreaObserver(const NotificationAreaObserver &) = delete;
231
    NotificationAreaObserver(NotificationAreaObserver &&) = delete;
232
    NotificationAreaObserver &operator=(const NotificationAreaObserver &) = delete;
233
    NotificationAreaObserver &operator=(NotificationAreaObserver &&) = delete;
234

235
    /// Function that is called by the console interface for this observer with the message
236
    /// information
237
    void SendLog(const std::string& notifiername, const std::string& msg, Base::LogStyle level,
238
                 Base::IntendedRecipient recipient, Base::ContentType content) override;
239

240
    /// Name of the observer
241
    const char* Name() override
242
    {
243
        return "NotificationAreaObserver";
244
    }
245

246
private:
247
    NotificationArea* notificationArea;
248
};
249

250
NotificationAreaObserver::NotificationAreaObserver(NotificationArea* notificationarea)
251
    : notificationArea(notificationarea)
252
{
253
    Base::Console().AttachObserver(this);
254
    bLog = false;        // ignore log messages
255
    bMsg = false;        // ignore messages
256
    bNotification = true;// activate user notifications
257
}
258

259
NotificationAreaObserver::~NotificationAreaObserver()
260
{
261
    Base::Console().DetachObserver(this);
262
}
263

264
void NotificationAreaObserver::SendLog(const std::string& notifiername, const std::string& msg,
265
                                       Base::LogStyle level, Base::IntendedRecipient recipient,
266
                                       Base::ContentType content)
267
{
268
    // 1. As notification system is shared with report view and others, the expectation is that any
269
    // individual error and warning message will end in "\n". This means the string must be stripped
270
    // of this character for representation in the notification area.
271
    // 2. However, any message marked with the QT_TRANSLATE_NOOT macro with the "Notifications"
272
    // context, shall not include
273
    // "\n", as this generates problems with the translation system. Then the string must be
274
    // stripped of "\n" before translation.
275

276
    bool violatesBasicPolicy = (recipient == Base::IntendedRecipient::Developer
277
                                || content == Base::ContentType::Untranslatable);
278

279
    // We allow derogations for debug purposes according to user preferences
280
    bool meetsDerogationCriteria = false;
281

282
    if (violatesBasicPolicy) {
283
        meetsDerogationCriteria =
284
            (level == Base::LogStyle::Warning && notificationArea->areDeveloperWarningsActive())
285
            || (level == Base::LogStyle::Error && notificationArea->areDeveloperErrorsActive());
286
    }
287

288
    if (violatesBasicPolicy && !meetsDerogationCriteria) {
289
        return;
290
    }
291

292
    auto simplifiedstring =
293
        QString::fromStdString(msg)
294
            .trimmed();// remove any leading and trailing whitespace character ('\n')
295

296
    // avoid processing empty strings
297
    if (simplifiedstring.isEmpty())
298
        return;
299

300
    if (content == Base::ContentType::Translated) {
301
        notificationArea->pushNotification(
302
            QString::fromStdString(notifiername), simplifiedstring, level);
303
    }
304
    else {
305
        notificationArea->pushNotification(
306
            QString::fromStdString(notifiername),
307
            QCoreApplication::translate("Notifications", simplifiedstring.toUtf8()),
308
            level);
309
    }
310
}
311

312

313
/******************* Notification Widget *******************************************************/
314

315
/** Specialised Item class for the Widget notifications/errors/warnings
316
   It holds all item specific data, including visualisation data and controls how
317
   the item should appear in the widget.
318
*/
319
class NotificationItem: public QTreeWidgetItem
320
{
321
public:
322
    NotificationItem(Base::LogStyle notificationtype, QString notifiername, QString message)
323
        : notificationType(notificationtype),
324
          notifierName(std::move(notifiername)),
325
          msg(std::move(message))
326
    {}
327

328
    QVariant data(int column, int role) const override
329
    {
330
        // strings that will be displayed for each column of the widget
331
        if (role == Qt::DisplayRole) {
332
            switch (column) {
333
                case 1:
334
                    return notifierName;
335
                    break;
336
                case 2:
337
                    return getMessage();
338
                    break;
339
            }
340
        }
341
        else if (column == 0 && role == Qt::DecorationRole) {
342
            // Icons to be displayed for the first row
343
            if (notificationType == Base::LogStyle::Error) {
344
                return std::move(ResourceManager::ErrorPixmap());
345
            }
346
            else if (notificationType == Base::LogStyle::Warning) {
347
                return std::move(ResourceManager::WarningPixmap());
348
            }
349
            else if (notificationType == Base::LogStyle::Critical) {
350
                return std::move(ResourceManager::CriticalPixmap());
351
            }
352
            else {
353
                return std::move(ResourceManager::InfoPixmap());
354
            }
355
        }
356
        else if (role == Qt::FontRole) {
357
            // Visualisation control of unread messages
358
            static QFont font;
359
            static QFont boldFont(font.family(), font.pointSize(), QFont::Bold);
360

361
            if (unread) {
362
                return boldFont;
363
            }
364

365
            return font;
366
        }
367

368
        return {};
369
    }
370

371
    void addRepetition() {
372
        unread = true;
373
        notifying = true;
374
        shown = false;
375
        repetitions++;
376
    }
377

378
    bool isRepeated(Base::LogStyle notificationtype, const QString & notifiername, const QString & message ) const {
379
        return (notificationType == notificationtype && notifierName == notifiername && msg == message);
380
    }
381

382
    bool isType(Base::LogStyle notificationtype) const {
383
        return notificationType == notificationtype;
384
    }
385

386
    bool isUnread() const {
387
        return unread;
388
    }
389

390
    bool isNotifying() const {
391
        return notifying;
392
    }
393

394
    bool isShown() const {
395
        return shown;
396
    }
397

398
    int getRepetitions() const{
399
        return repetitions;
400
    }
401

402
    void setNotified() {
403
        notifying = false;
404
    }
405

406
    void resetNotified() {
407
        notifying = true;
408
    }
409

410
    void setShown() {
411
        shown = true;
412
    }
413

414
    void resetShown() {
415
        shown = false;
416
    }
417

418
    void setRead() {
419
        unread = false;
420
    }
421

422
    void setUnread() {
423
        unread = true;
424
    }
425

426
    QString getMessage() const {
427
        if(repetitions == 0) {
428
            return msg;
429
        }
430
        else {
431
            return msg + QObject::tr(" (%1 times)").arg(repetitions+1);
432
        }
433
    }
434

435
    const QString & getNotifier() {
436
        return notifierName;
437
    }
438

439
private:
440
    Base::LogStyle notificationType;
441
    QString notifierName;
442
    QString msg;
443

444
    bool unread = true;   // item is unread in the Notification Area Widget
445
    bool notifying = true;// item is to be notified or being notified as non-intrusive message
446
    bool shown = false;   // item is already being notified (it is onScreen)
447
    int repetitions = 0; // message appears n times in a row.
448
};
449

450
/** Drop menu Action containing the notifications widget.
451
 *  It stores all the notification item information in the form
452
 * of NotificationItems (QTreeWidgetItem). This information is used
453
 * by the Widget and by the non-intrusive messages.
454
 * It owns the notification resources and is responsible for the release
455
 * of the memory resources, either directly for the intermediate fast cache
456
 * or indirectly via QT for the case of the QTreeWidgetItems.
457
 */
458
class NotificationsAction: public QWidgetAction
459
{
460
public:
461
    NotificationsAction(QWidget* parent)
462
        : QWidgetAction(parent)
463
    {}
464

465
    NotificationsAction(const NotificationsAction &) = delete;
466
    NotificationsAction(NotificationsAction &&) = delete;
467
    NotificationsAction & operator=(const NotificationsAction &) = delete;
468
    NotificationsAction & operator=(NotificationsAction &&) = delete;
469

470
    ~NotificationsAction() override
471
    {
472
        for (auto* item : std::as_const(pushedItems)) {
473
            if (item) {
474
                delete item; // NOLINT
475
            }
476
        }
477
    }
478

479
public:
480
    /// deletes only notifications (messages of type Notification)
481
    void deleteNotifications()
482
    {
483
        if (tableWidget) {
484
            for (int i = tableWidget->topLevelItemCount() - 1; i >= 0; i--) {
485
                //NOLINTNEXTLINE
486
                auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
487
                if (item->isType(Base::LogStyle::Notification)) {
488
                    delete item; //NOLINT
489
                }
490
            }
491
        }
492
        for (int i = pushedItems.size() - 1; i >= 0; i--) {
493
            //NOLINTNEXTLINE
494
            auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
495
            if (item->isType(Base::LogStyle::Notification)) {
496
                delete pushedItems.takeAt(i); //NOLINT
497
            }
498
        }
499
    }
500

501
    /// deletes all notifications, errors and warnings
502
    void deleteAll()
503
    {
504
        if (tableWidget) {
505
            tableWidget->clear();// the parent will delete the items.
506
        }
507
        while (!pushedItems.isEmpty())
508
            delete pushedItems.takeFirst();
509
    }
510

511
    /// returns the amount of unread notifications, errors and warnings
512
    inline int getUnreadCount() const
513
    {
514
        return getCurrently([](auto* item) {
515
            return item->isUnread();
516
        });
517
    }
518

519
    /// returns the amount of notifications, errors and warnings currently being notified
520
    inline int getCurrentlyNotifyingCount() const
521
    {
522
        return getCurrently([](auto* item) {
523
            return item->isNotifying();
524
        });
525
    }
526

527
    /// returns the amount of notifications, errors and warnings currently being shown as
528
    /// non-intrusive messages (on-screen)
529
    inline int getShownCount() const
530
    {
531
        return getCurrently([](auto* item) {
532
            return item->isShown();
533
        });
534
    }
535

536
    /// marks all notifications, errors and warnings as read
537
    void clearUnreadFlag()
538
    {
539
        for (auto i = 0; i < tableWidget->topLevelItemCount();
540
             i++) {// all messages were read, so clear the unread flag
541
            //NOLINTNEXTLINE
542
            auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
543
            item->setRead();
544
        }
545
    }
546

547
    /// pushes all Notification Items to the Widget, so that they can be shown
548
    void synchroniseWidget()
549
    {
550
        tableWidget->insertTopLevelItems(0, pushedItems);
551
        pushedItems.clear();
552
    }
553

554
    /** pushes all Notification Items to the fast cache (this also prevents all unnecessary
555
     * signaling from parents) and allows to accelerate insertions and deletions
556
     */
557
    void shiftToCache()
558
    {
559
        tableWidget->blockSignals(true);
560
        tableWidget->clearSelection();
561
        while (tableWidget->topLevelItemCount() > 0) {
562
            auto* item = tableWidget->takeTopLevelItem(0);
563
            pushedItems.push_back(item);
564
        }
565
        tableWidget->blockSignals(false);
566
    }
567

568
    /// returns if there are no notifications, errors and warnings at all
569
    bool isEmpty() const
570
    {
571
        return tableWidget->topLevelItemCount() == 0 && pushedItems.isEmpty();
572
    }
573

574
    /// returns the total amount of notifications, errors and warnings currently stored
575
    auto count() const
576
    {
577
        return tableWidget->topLevelItemCount() + pushedItems.count();
578
    }
579

580
    /// retrieves a pointer to a given notification from storage.
581
    auto getItem(int index) const
582
    {
583
        if (index < pushedItems.count()) {
584
            return pushedItems.at(index);
585
        }
586
        else {
587
            return tableWidget->topLevelItem(index - pushedItems.count());
588
        }
589
    }
590

591
    /// deletes a given notification, errors or warnings by index
592
    void deleteItem(int index)
593
    {
594
        if (index < pushedItems.count()) {
595
            delete pushedItems.takeAt(index); //NOLINT
596
        }
597
        else {
598
            delete tableWidget->topLevelItem(index - pushedItems.count()); //NOLINT
599
        }
600
    }
601

602
    /// deletes a given notification, errors or warnings by pointer
603
    void deleteItem(NotificationItem* item)
604
    {
605
        for (int i = 0; i < count(); i++) {
606
            if (getItem(i) == item) {
607
                deleteItem(i);
608
                return;
609
            }
610
        }
611
    }
612

613
    /// deletes the last Notification Item
614
    void deleteLastItem()
615
    {
616
        deleteItem(count() - 1);
617
    }
618

619
    /// checks if last notification is the same
620
    bool isSameNotification(const QString& notifiername, const QString& message,
621
                            Base::LogStyle level) const {
622
        if(count() > 0) { // if not empty
623
            //NOLINTNEXTLINE
624
            auto item = static_cast<NotificationItem*>(getItem(0));
625
            return item->isRepeated(level,notifiername,message);
626
        }
627

628
        return false;
629
    }
630

631
    void resetLastNotificationStatus() {
632
        //NOLINTNEXTLINE
633
        auto item = static_cast<NotificationItem*>(getItem(0));
634
        item->addRepetition();
635
    }
636

637
    /// pushes a notification item to the front
638
    auto push_front(std::unique_ptr<NotificationItem> item)
639
    {
640
        auto it = item.release();
641
        pushedItems.push_front(it);
642
        return it;
643
    }
644

645
    QSize size()
646
    {
647
        return tableWidget->size();
648
    }
649

650
protected:
651
    /// creates the Notifications Widget
652
    QWidget* createWidget(QWidget* parent) override
653
    {
654
        //NOLINTBEGIN
655
        QWidget* notificationsWidget = new QWidget(parent);
656

657
        QHBoxLayout* layout = new QHBoxLayout(notificationsWidget);
658
        notificationsWidget->setLayout(layout);
659

660
        tableWidget = new QTreeWidget(parent);
661
        tableWidget->setColumnCount(3);
662

663
        QStringList headers;
664
        headers << QObject::tr("Type") << QObject::tr("Notifier") << QObject::tr("Message");
665
        tableWidget->setHeaderLabels(headers);
666

667
        layout->addWidget(tableWidget);
668

669
        tableWidget->setMaximumSize(1200, 600);
670
        tableWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
671
        tableWidget->header()->setStretchLastSection(false);
672
        tableWidget->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
673

674
        tableWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
675
        tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
676

677

678
        // context menu on any item (row) of the widget
679
        QObject::connect(
680
            tableWidget, &QTreeWidget::customContextMenuRequested, [&](const QPoint& pos) {
681
                auto selectedItems = tableWidget->selectedItems();
682

683
                QMenu menu;
684

685
                QAction* del = menu.addAction(tr("Delete"), this, [&]() {
686
                    for (auto it : std::as_const(selectedItems)) {
687
                        delete it;
688
                    }
689
                });
690

691
                del->setEnabled(!selectedItems.isEmpty());
692

693
                menu.addSeparator();
694

695
                QAction* delnotifications =
696
                    menu.addAction(tr("Delete user notifications"),
697
                                   this,
698
                                   &NotificationsAction::deleteNotifications);
699

700
                delnotifications->setEnabled(tableWidget->topLevelItemCount() > 0);
701

702
                QAction* delall =
703
                    menu.addAction(tr("Delete All"), this, &NotificationsAction::deleteAll);
704

705
                delall->setEnabled(tableWidget->topLevelItemCount() > 0);
706

707
                menu.setDefaultAction(del);
708

709
                menu.exec(tableWidget->mapToGlobal(pos));
710
            });
711
        //NOLINTEND
712

713
        return notificationsWidget;
714
    }
715

716
private:
717
    /// utility function to return the number of Notification Items meeting the functor/lambda
718
    /// criteria
719
    int getCurrently(std::function<bool(const NotificationItem*)> F) const
720
    {
721
        int instate = 0;
722
        for (auto i = 0; i < tableWidget->topLevelItemCount(); i++) {
723
            //NOLINTNEXTLINE
724
            auto* item = static_cast<NotificationItem*>(tableWidget->topLevelItem(i));
725
            if (F(item)) {
726
                instate++;
727
            }
728
        }
729
        for (auto i = 0; i < pushedItems.count(); i++) {
730
             //NOLINTNEXTLINE
731
            auto* item = static_cast<NotificationItem*>(pushedItems.at(i));
732
            if (F(item)) {
733
                instate++;
734
            }
735
        }
736
        return instate;
737
    }
738

739
private:
740
    QTreeWidget* tableWidget = nullptr;
741
    // Intermediate storage
742
    // Note: QTreeWidget is helplessly slow to single insertions, QTreeWidget is actually only
743
    // necessary when showing the widget. A single QList insertion into a QTreeWidget is actually
744
    // not that slow. The use of this intermediate storage substantially accelerates non-intrusive
745
    // notifications.
746
    QList<QTreeWidgetItem*> pushedItems;
747
};
748

749
/************ Parameter Observer (preferences) **************************************/
750

751
NotificationArea::ParameterObserver::ParameterObserver(NotificationArea* notificationarea)
752
    : notificationArea(notificationarea)
753
{
754
    hGrp = App::GetApplication().GetParameterGroupByPath(
755
        "User parameter:BaseApp/Preferences/NotificationArea");
756

757
    //NOLINTBEGIN
758
    parameterMap = {
759
        {"NotificationAreaEnabled",
760
         [this](const std::string& string) {
761
             auto enabled = hGrp->GetBool(string.c_str(), true);
762
             if (!enabled)
763
                 notificationArea->deleteLater();
764
         }},
765
        {"NonIntrusiveNotificationsEnabled",
766
         [this](const std::string& string) {
767
             auto enabled = hGrp->GetBool(string.c_str(), true);
768
             notificationArea->pImp->notificationsDisabled = !enabled;
769
         }},
770
        {"NotificationTime",
771
         [this](const std::string& string) {
772
             auto time = hGrp->GetInt(string.c_str(), 20) * 1000;
773
             if (time < 0)
774
                 time = 0;
775
             notificationArea->pImp->notificationExpirationTime = static_cast<unsigned int>(time);
776
         }},
777
        {"MinimumOnScreenTime",
778
         [this](const std::string& string) {
779
             auto time = hGrp->GetInt(string.c_str(), 5) * 1000;
780
             if (time < 0)
781
                 time = 0;
782
             notificationArea->pImp->minimumOnScreenTime = static_cast<unsigned int>(time);
783
         }},
784
        {"MaxOpenNotifications",
785
         [this](const std::string& string) {
786
             auto limit = hGrp->GetInt(string.c_str(), 15);
787
             if (limit < 0)
788
                 limit = 0;
789
             notificationArea->pImp->maxOpenNotifications = static_cast<unsigned int>(limit);
790
         }},
791
        {"MaxWidgetMessages",
792
         [this](const std::string& string) {
793
             auto limit = hGrp->GetInt(string.c_str(), 1000);
794
             if (limit < 0)
795
                 limit = 0;
796
             notificationArea->pImp->maxWidgetMessages = static_cast<unsigned int>(limit);
797
         }},
798
        {"AutoRemoveUserNotifications",
799
         [this](const std::string& string) {
800
             auto enabled = hGrp->GetBool(string.c_str(), true);
801
             notificationArea->pImp->autoRemoveUserNotifications = enabled;
802
         }},
803
        {"NotificiationWidth",
804
         [this](const std::string& string) {
805
             auto width = hGrp->GetInt(string.c_str(), 800);
806
             if (width < 300)
807
                 width = 300;
808
             notificationArea->pImp->notificationWidth = width;
809
         }},
810
        {"HideNonIntrusiveNotificationsWhenWindowDeactivated",
811
         [this](const std::string& string) {
812
             auto enabled = hGrp->GetBool(string.c_str(), true);
813
             notificationArea->pImp->hideNonIntrusiveNotificationsWhenWindowDeactivated = enabled;
814
         }},
815
        {"PreventNonIntrusiveNotificationsWhenWindowNotActive",
816
         [this](const std::string& string) {
817
             auto enabled = hGrp->GetBool(string.c_str(), true);
818
             notificationArea->pImp->preventNonIntrusiveNotificationsWhenWindowNotActive = enabled;
819
         }},
820
        {"DeveloperErrorSubscriptionEnabled",
821
         [this](const std::string& string) {
822
             auto enabled = hGrp->GetBool(string.c_str(), false);
823
             notificationArea->pImp->developerErrorSubscriptionEnabled = enabled;
824
         }},
825
        {"DeveloperWarningSubscriptionEnabled",
826
         [this](const std::string& string) {
827
             auto enabled = hGrp->GetBool(string.c_str(), false);
828
             notificationArea->pImp->developerWarningSubscriptionEnabled = enabled;
829
         }},
830
    };
831
    //NOLINTEND
832

833
    for (auto& val : parameterMap) {
834
        auto string = val.first;
835
        auto update = val.second;
836

837
        update(string);
838
    }
839

840
    hGrp->Attach(this);
841
}
842

843
NotificationArea::ParameterObserver::~ParameterObserver()
844
{
845
    hGrp->Detach(this);
846
}
847

848
void NotificationArea::ParameterObserver::OnChange(Base::Subject<const char*>& rCaller,
849
                                                   const char* sReason)
850
{
851
    (void)rCaller;
852

853
    auto key = parameterMap.find(sReason);
854

855
    if (key != parameterMap.end()) {
856
        auto string = key->first;
857
        auto update = key->second;
858

859
        update(string);
860
    }
861
}
862

863
/************************* Notification Area *****************************************/
864

865
NotificationArea::NotificationArea(QWidget* parent)
866
    : QPushButton(parent)
867
{
868
    // QPushButton appearance
869
    setText(QString());
870
    setFlat(true);
871

872
    // Initialisation of pImpl structure
873
    pImp = std::make_unique<NotificationAreaP>();
874

875
    pImp->observer = std::make_unique<NotificationAreaObserver>(this);
876
    pImp->parameterObserver = std::make_unique<NotificationArea::ParameterObserver>(this);
877

878
    pImp->menu = new QMenu(parent); //NOLINT
879
    setMenu(pImp->menu);
880

881
    auto na = new NotificationsAction(pImp->menu); //NOLINT
882

883
    pImp->menu->addAction(na);
884

885
    pImp->notificationaction = na;
886

887
    //NOLINTBEGIN
888
    // Signals for synchronisation of storage before showing/hiding the widget
889
    QObject::connect(pImp->menu, &QMenu::aboutToHide, [&]() {
890
        lock_guard<std::mutex> g(pImp->mutexNotification);
891
        static_cast<NotificationsAction*>(pImp->notificationaction)->clearUnreadFlag();
892
        static_cast<NotificationsAction*>(pImp->notificationaction)->shiftToCache();
893
    });
894

895
    QObject::connect(pImp->menu, &QMenu::aboutToShow, [this]() {
896
        // guard to avoid modifying the notification list and indices while creating the tooltip
897
        lock_guard<std::mutex> g(pImp->mutexNotification);
898
        setText(QString::number(0)); // no unread notifications
899
        if (pImp->missedNotifications) {
900
            setIcon(TrayIcon::Normal);
901
            pImp->missedNotifications = false;
902
        }
903
        static_cast<NotificationsAction*>(pImp->notificationaction)->synchroniseWidget();
904

905

906
        // There is a Qt bug in not respecting a QMenu size when size changes in aboutToShow.
907
        //
908
        // https://bugreports.qt.io/browse/QTBUG-54421
909
        // https://forum.qt.io/topic/68765/how-to-update-geometry-on-qaction-visibility-change-in-qmenu-abouttoshow/3
910
        //
911
        // None of this works
912
        // pImp->menu->updateGeometry();
913
        // pImp->menu->adjustSize();
914
        // pImp->menu->ensurePolished();
915
        // this->updateGeometry();
916
        // this->adjustSize();
917
        // this->ensurePolished();
918

919
        // This does correct the size
920
        QSize size = static_cast<NotificationsAction*>(pImp->notificationaction)->size();
921
        QResizeEvent re(size, size);
922
        qApp->sendEvent(pImp->menu, &re);
923

924
        // This corrects the position of the menu
925
        QTimer::singleShot(0, [&] {
926
            QWidget* statusbar = static_cast<QWidget*>(this->parent());
927
            QPoint statusbar_top_right = statusbar->mapToGlobal(statusbar->rect().topRight());
928
            QSize menusize = pImp->menu->size();
929
            QWidget* w = this;
930
            QPoint button_pos = w->mapToGlobal(w->rect().topLeft());
931
            QPoint widget_pos;
932
            if ((statusbar_top_right.x() - menusize.width()) > button_pos.x()) {
933
                widget_pos = QPoint(button_pos.x(), statusbar_top_right.y() - menusize.height());
934
            }
935
            else {
936
                widget_pos = QPoint(statusbar_top_right.x() - menusize.width(),
937
                                    statusbar_top_right.y() - menusize.height());
938
            }
939
            pImp->menu->move(widget_pos);
940
        });
941
    });
942

943
    // Connection to the finish restore signal to rearm Critical messages modal mode when action is
944
    // user initiated
945
    pImp->finishRestoreDocumentConnection =
946
        App::GetApplication().signalFinishRestoreDocument.connect(
947
            std::bind(&Gui::NotificationArea::slotRestoreFinished, this, sp::_1));
948
    //NOLINTEND
949

950
    // Initialisation of the timer to inhibit continuous updates of the notification system in case
951
    // clusters of messages arrive (so as to delay the actual notification until the whole cluster
952
    // has arrived)
953
    pImp->inhibitTimer.setSingleShot(true);
954

955
    pImp->inhibitTimer.callOnTimeout([this, na]() {
956
        setText(QString::number(na->getUnreadCount()));
957
        showInNotificationArea();
958
    });
959

960
    setIcon(TrayIcon::Normal);
961
}
962

963
NotificationArea::~NotificationArea()
964
{
965
    pImp->finishRestoreDocumentConnection.disconnect();
966
}
967

968
void NotificationArea::mousePressEvent(QMouseEvent* e)
969
{
970
    // Contextual menu (e.g. to delete Notifications or all messages (Notifications, Errors,
971
    // Warnings and Critical messages)
972
    if (e->button() == Qt::RightButton && hitButton(e->pos())) {
973
        QMenu menu;
974

975
        //NOLINTBEGIN
976
        NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
977

978
        QAction* delnotifications = menu.addAction(tr("Delete user notifications"), [&]() {
979
            // guard to avoid modifying the notification list and indices while creating the tooltip
980
            lock_guard<std::mutex> g(pImp->mutexNotification);
981
            na->deleteNotifications();
982
            setText(QString::number(na->getUnreadCount()));
983
        });
984

985
        delnotifications->setEnabled(!na->isEmpty());
986

987
        QAction* delall = menu.addAction(tr("Delete All"), [&]() {
988
            // guard to avoid modifying the notification list and indices while creating the tooltip
989
            lock_guard<std::mutex> g(pImp->mutexNotification);
990
            na->deleteAll();
991
            setText(QString::number(0));
992
        });
993
        //NOLINTEND
994

995
        delall->setEnabled(!na->isEmpty());
996

997
        menu.setDefaultAction(delall);
998

999
        menu.exec(this->mapToGlobal(e->pos()));
1000
    }
1001
    QPushButton::mousePressEvent(e);
1002
}
1003

1004
bool NotificationArea::areDeveloperWarningsActive() const
1005
{
1006
    return pImp->developerWarningSubscriptionEnabled;
1007
}
1008

1009
bool NotificationArea::areDeveloperErrorsActive() const
1010
{
1011
    return pImp->developerErrorSubscriptionEnabled;
1012
}
1013

1014
void NotificationArea::pushNotification(const QString& notifiername, const QString& message,
1015
                                        Base::LogStyle level)
1016
{
1017
    auto confirmation = confirmationRequired(level);
1018

1019
    if (confirmation) {
1020
        showConfirmationDialog(notifiername, message);
1021
    }
1022
    // guard to avoid modifying the notification list and indices while creating the tooltip
1023
    lock_guard<std::mutex> g(pImp->mutexNotification);
1024

1025
    //NOLINTNEXTLINE
1026
    NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
1027

1028
    // Limit the maximum number of messages stored in the widget (0 means no limit)
1029
    if (pImp->maxWidgetMessages != 0 && na->count() > pImp->maxWidgetMessages) {
1030
        na->deleteLastItem();
1031
    }
1032

1033
    auto repeated = na->isSameNotification(notifiername, message, level);
1034

1035
    if(!repeated) {
1036
        auto itemptr = std::make_unique<NotificationItem>(level, notifiername, message);
1037

1038
        auto item = na->push_front(std::move(itemptr));
1039

1040
        // If the non-intrusive notifications are disabled then stop here (messages added to the widget
1041
        // only)
1042
        if (pImp->notificationsDisabled) {
1043
            item->setNotified(); // avoid mass of old notifications if feature is activated afterwards
1044
            //NOLINTBEGIN
1045
            setText(QString::number(
1046
                static_cast<NotificationsAction*>(pImp->notificationaction)->getUnreadCount()));
1047
            return;
1048
            //NOLINTEND
1049
        }
1050
    }
1051
    else {
1052
        na->resetLastNotificationStatus();
1053
    }
1054

1055
    // start or restart rate control (the timer is rearmed if not yet expired, expiration triggers
1056
    // showing of the notification messages)
1057
    //
1058
    // NOTE: The inhibition timer is created in the main thread and cannot be restarted from another
1059
    // QThread. A QTimer can be moved to another QThread (from the QThread in which it is). However,
1060
    // it can only be create in a QThread, not in any other thread.
1061
    //
1062
    // For this reason, the timer is only triggered if this QThread is the QTimer thread.
1063
    //
1064
    // But I want my message from my thread to appear in the notification area. Fine, then configure
1065
    // Console not to use the direct connection mode, but the Queued one:
1066
    // Base::Console().SetConnectionMode(ConnectionMode::Queued);
1067

1068
    auto timer_thread = pImp->inhibitTimer.thread();
1069
    auto current_thread = QThread::currentThread();
1070

1071
    if (timer_thread == current_thread) {
1072
        pImp->inhibitTimer.start(static_cast<int>(pImp->inhibitNotificationTime));
1073
    }
1074
}
1075

1076
bool NotificationArea::confirmationRequired(Base::LogStyle level)
1077
{
1078
    auto userInitiatedRestore =
1079
        Application::Instance->testStatus(Gui::Application::UserInitiatedOpenDocument);
1080

1081
    return (level == Base::LogStyle::Critical && userInitiatedRestore
1082
            && pImp->requireConfirmationCriticalMessageDuringRestoring);
1083
}
1084

1085
void NotificationArea::showConfirmationDialog(const QString& notifiername, const QString& message)
1086
{
1087
    auto confirmMsg = QObject::tr("Notifier:") + QStringLiteral(" ") + notifiername + QStringLiteral("\n\n") + message
1088
        + QStringLiteral("\n\n")
1089
        + QObject::tr("Do you want to skip confirmation of further critical message notifications "
1090
                      "while loading the file?");
1091

1092
    auto button = QMessageBox::critical(getMainWindow()->activeWindow(),
1093
                                        QObject::tr("Critical Message"),
1094
                                        confirmMsg,
1095
                                        QMessageBox::Yes | QMessageBox::No,
1096
                                        QMessageBox::No);
1097

1098
    if (button == QMessageBox::Yes)
1099
        pImp->requireConfirmationCriticalMessageDuringRestoring = false;
1100
}
1101

1102
void NotificationArea::showInNotificationArea()
1103
{
1104
    // guard to avoid modifying the notification list and indices while creating the tooltip
1105
    lock_guard<std::mutex> g(pImp->mutexNotification);
1106

1107
    //NOLINTNEXTLINE
1108
    NotificationsAction* na = static_cast<NotificationsAction*>(pImp->notificationaction);
1109

1110
    if (!NotificationBox::isVisible()) {
1111
        // The Notification Box may have been closed (by the user by popping it out or by left mouse
1112
        // button) ensure that old notifications are not shown again, even if the timer has not
1113
        // lapsed
1114
        int i = 0;
1115
        //NOLINTNEXTLINE
1116
        while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->isNotifying()) {
1117
            //NOLINTNEXTLINE
1118
            NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
1119

1120
            if (item->isShown()) {
1121
                item->setNotified();
1122
                item->resetShown();
1123
            }
1124

1125
            i++;
1126
        }
1127
    }
1128

1129
    auto currentlyshown = na->getShownCount();
1130

1131
    // If we cannot show more messages, we do no need to update the non-intrusive notification
1132
    if (currentlyshown < pImp->maxOpenNotifications) {
1133
        // There is space for at least one more notification
1134
        // We update the message with the most recent up to maxOpenNotifications
1135

1136
        QString msgw =
1137
            QString::fromLatin1(
1138
                "<style>p { margin: 0 0 0 0 } td { padding: 0 15px }</style>                     \
1139
        <p style='white-space:normal'>                                                                                      \
1140
        <table>                                                                                                             \
1141
        <tr>                                                                                                               \
1142
        <th><small>%1</small></th>                                                                                        \
1143
        <th><small>%2</small></th>                                                                                        \
1144
        <th><small>%3</small></th>                                                                                        \
1145
        </tr>")
1146
                .arg(QObject::tr("Type"), QObject::tr("Notifier"), QObject::tr("Message"));
1147

1148
        auto currentlynotifying = na->getCurrentlyNotifyingCount();
1149

1150
        if (currentlynotifying > pImp->maxOpenNotifications) {
1151
            msgw +=
1152
                QString::fromLatin1(
1153
                    "                                                                                   \
1154
            <tr>                                                                                                            \
1155
            <td align='left'><img width=\"16\" height=\"16\" src=':/icons/Warning.svg'></td>                                \
1156
            <td align='left'>FreeCAD</td>                                                                                   \
1157
            <td align='left'>%1</td>                                                                                        \
1158
            </tr>")
1159
                    .arg(QObject::tr("Too many opened non-intrusive notifications. Notifications "
1160
                                     "are being omitted!"));
1161
        }
1162

1163
        int i = 0;
1164

1165
        //NOLINTNEXTLINE
1166
        while (i < na->count() && static_cast<NotificationItem*>(na->getItem(i))->isNotifying()) {
1167

1168
            if (i < pImp->maxOpenNotifications) {// show the first up to maxOpenNotifications
1169
                //NOLINTNEXTLINE
1170
                NotificationItem* item = static_cast<NotificationItem*>(na->getItem(i));
1171

1172
                QString iconstr;
1173
                if (item->isType(Base::LogStyle::Error)) {
1174
                    iconstr = QStringLiteral(":/icons/edit_Cancel.svg");
1175
                }
1176
                else if (item->isType(Base::LogStyle::Warning)) {
1177
                    iconstr = QStringLiteral(":/icons/Warning.svg");
1178
                }
1179
                else if (item->isType(Base::LogStyle::Critical)) {
1180
                    iconstr = QStringLiteral(":/icons/critical-info.svg");
1181
                }
1182
                else {
1183
                    iconstr = QStringLiteral(":/icons/info.svg");
1184
                }
1185

1186
                QString tmpmessage =
1187
                    convertFromPlainText(item->getMessage(), Qt::WhiteSpaceMode::WhiteSpaceNormal);
1188

1189
                msgw +=
1190
                    QString::fromLatin1(
1191
                        "                                                                                   \
1192
                <tr>                                                                                                            \
1193
                <td align='left'><img width=\"16\" height=\"16\" src='%1'></td>                                                 \
1194
                <td align='left'>%2</td>                                                                                        \
1195
                <td align='left'>%3</td>                                                                                        \
1196
                </tr>")
1197
                        .arg(iconstr, item->getNotifier(), tmpmessage);
1198

1199
                // start a timer for each of these notifications that was not previously shown
1200
                if (!item->isShown()) {
1201
                    QTimer::singleShot(pImp->notificationExpirationTime, [this, item, repetitions = item->getRepetitions()]() {
1202
                        // guard to avoid modifying the notification
1203
                        // start index while creating the tooltip
1204
                        lock_guard<std::mutex> g(pImp->mutexNotification);
1205

1206
                        // if the item exists and the number of repetitions has not changed in the
1207
                        // meantime
1208
                        if (item && item->getRepetitions() == repetitions) {
1209
                            item->resetShown();
1210
                            item->setNotified();
1211

1212
                            if (pImp->autoRemoveUserNotifications) {
1213
                                if (item->isType(Base::LogStyle::Notification)) {
1214
                                    //NOLINTNEXTLINE
1215
                                    static_cast<NotificationsAction*>(pImp->notificationaction)
1216
                                        ->deleteItem(item);
1217
                                }
1218
                            }
1219
                        }
1220
                    });
1221
                }
1222

1223
                // We update the status to shown
1224
                item->setShown();
1225
            }
1226
            else {// We do not have more space and older notifications will be too old
1227
                //NOLINTBEGIN
1228
                static_cast<NotificationItem*>(na->getItem(i))->setNotified();
1229
                static_cast<NotificationItem*>(na->getItem(i))->resetShown();
1230
                //NOLINTEND
1231
            }
1232

1233
            i++;
1234
        }
1235

1236
        msgw += QString::fromLatin1("</table></p>");
1237

1238
        NotificationBox::Options options = NotificationBox::Options::RestrictAreaToReference;
1239

1240
        if (pImp->preventNonIntrusiveNotificationsWhenWindowNotActive) {
1241
            options = options | NotificationBox::Options::OnlyIfReferenceActive;
1242
        }
1243

1244
        if (pImp->hideNonIntrusiveNotificationsWhenWindowDeactivated) {
1245
            options = options | NotificationBox::Options::HideIfReferenceWidgetDeactivated;
1246
        }
1247

1248
        bool isshown = NotificationBox::showText(this->mapToGlobal(QPoint()),
1249
                                                 msgw,
1250
                                                 getMainWindow(),
1251
                                                 static_cast<int>(pImp->notificationExpirationTime),
1252
                                                 pImp->minimumOnScreenTime,
1253
                                                 options,
1254
                                                 pImp->notificationWidth);
1255

1256
        if (!isshown && !pImp->missedNotifications) {
1257
            pImp->missedNotifications = true;
1258
            setIcon(TrayIcon::MissedNotifications);
1259
        }
1260
    }
1261
}
1262

1263
void NotificationArea::slotRestoreFinished(const App::Document&)
1264
{
1265
    // Re-arm on restore critical message modal notifications if another document is loaded
1266
    pImp->requireConfirmationCriticalMessageDuringRestoring = true;
1267
}
1268

1269
void NotificationArea::setIcon(TrayIcon trayIcon)
1270
{
1271
    if (trayIcon == TrayIcon::Normal) {
1272
        QPushButton::setIcon(ResourceManager::NotificationAreaIcon());
1273
    }
1274
    else if (trayIcon == TrayIcon::MissedNotifications) {
1275
        QPushButton::setIcon(ResourceManager::notificationAreaMissedNotificationsIcon());
1276
    }
1277
}
1278

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.