File indexing completed on 2024-04-21 09:17:13

0001 /*
0002     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0003     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "statusnotifier.h"
0009 
0010 #include <QAction>
0011 #include <QDBusConnectionInterface>
0012 #include <QDBusServiceWatcher>
0013 #include <QMenu>
0014 
0015 #include <KLocalizedString>
0016 #include <KNotification>
0017 #include <KStatusNotifierItem>
0018 
0019 #include "crashedapplication.h"
0020 #include "drkonqi.h"
0021 #include "statusnotifier_activationclosetimer.h"
0022 
0023 namespace
0024 {
0025 QString activationMessage(StatusNotifier::Activation activation)
0026 {
0027     switch (activation) {
0028     case StatusNotifier::Activation::NotAllowed:
0029         return i18nc("Notification text", "The application closed unexpectedly.");
0030     case StatusNotifier::Activation::Allowed:
0031         return i18nc("Notification text", "Please report this error to help improve this software.");
0032     case StatusNotifier::Activation::AlreadySubmitting:
0033         return i18nc("Notification text", "The application closed unexpectedly. A report is being automatically submitted.");
0034     }
0035     Q_ASSERT(false);
0036     return {};
0037 }
0038 } // namespace
0039 
0040 StatusNotifier::StatusNotifier(QObject *parent)
0041     : QObject(parent)
0042     , m_sni(new KStatusNotifierItem(this))
0043 {
0044     CrashedApplication *crashedApp = DrKonqi::crashedApplication();
0045 
0046     // this is used for both the SNI tooltip as well as the notification
0047     m_title = i18nc("Placeholder is an application name; it crashed", "%1 Closed Unexpectedly", crashedApp->name());
0048 }
0049 
0050 StatusNotifier::~StatusNotifier() = default;
0051 
0052 void StatusNotifier::show()
0053 {
0054     CrashedApplication *crashedApp = DrKonqi::crashedApplication();
0055 
0056     connect(this, &StatusNotifier::activated, this, &StatusNotifier::deleteLater);
0057     // The expiring the notifier doesn't necessarily quit immediately, make sure to hide the SNI by deleting it (there
0058     // is no way to hide an SNI).
0059     connect(this, &StatusNotifier::expired, m_sni, &QObject::deleteLater);
0060 
0061     m_sni->setTitle(m_title);
0062     m_sni->setIconByName(QStringLiteral("tools-report-bug"));
0063     m_sni->setStatus(KStatusNotifierItem::Active);
0064     m_sni->setCategory(KStatusNotifierItem::SystemServices);
0065     m_sni->setToolTip(m_sni->iconName(), m_sni->title(), i18n("Please report this error to help improve this software."));
0066     connect(m_sni, &KStatusNotifierItem::activateRequested, this, &StatusNotifier::activated);
0067 
0068     // you cannot turn off that "Do you really want to quit?" message, so we'll add our own below
0069     m_sni->setStandardActionsEnabled(false);
0070 
0071     auto *sniMenu = new QMenu();
0072     auto *action = new QAction(QIcon::fromTheme(QStringLiteral("tools-report-bug")), i18n("Report &Bug"), nullptr);
0073     connect(action, &QAction::triggered, this, &StatusNotifier::activated);
0074     sniMenu->addAction(action);
0075     sniMenu->setDefaultAction(action);
0076 
0077     if (canBeRestarted(crashedApp)) {
0078         action = new QAction(QIcon::fromTheme(QStringLiteral("system-reboot")), i18n("&Restart Application"), nullptr);
0079         connect(action, &QAction::triggered, crashedApp, &CrashedApplication::restart);
0080         // once restarted successfully, disable restart option
0081         connect(crashedApp, &CrashedApplication::restarted, action, [action](bool success) {
0082             action->setEnabled(!success);
0083         });
0084         sniMenu->addAction(action);
0085     }
0086 
0087     sniMenu->addSeparator();
0088 
0089     action = new QAction(QIcon::fromTheme(QStringLiteral("application-exit")), i18nc("Allows the user to hide this notifier item", "Hide"), nullptr);
0090     connect(action, &QAction::triggered, this, &StatusNotifier::expired);
0091     sniMenu->addAction(action);
0092 
0093     m_sni->setContextMenu(sniMenu);
0094 
0095     // Depending on the environmental state we may auto-activate or auto-close the SNI.
0096     auto timer = new ActivationCloseTimer(this);
0097     connect(timer, &ActivationCloseTimer::autoActivate, this, &StatusNotifier::activated);
0098     connect(timer, &ActivationCloseTimer::autoClose, this, &StatusNotifier::expired);
0099     auto dbusWatcher = new DBusServiceWatcher(timer);
0100     auto idleWatcher = new IdleWatcher(timer);
0101     timer->start(dbusWatcher, idleWatcher);
0102 }
0103 
0104 void StatusNotifier::notify(Activation activation)
0105 {
0106     CrashedApplication *crashedApp = DrKonqi::crashedApplication();
0107 
0108     const auto activationAllowed = activation != Activation::NotAllowed;
0109     const QString title = activationAllowed ? m_title : crashedApp->name();
0110     const QString message = activationMessage(activation);
0111 
0112     KNotification *notification = KNotification::event(QStringLiteral("applicationcrash"),
0113                                                        title,
0114                                                        message,
0115                                                        QStringLiteral("tools-report-bug"),
0116                                                        KNotification::DefaultEvent | KNotification::SkipGrouping);
0117 
0118     if (activationAllowed) {
0119         if (activation == StatusNotifier::Activation::AlreadySubmitting) {
0120             auto details = notification->addAction(i18nc("@action:button, keep short", "Add Details"));
0121             connect(details, &KNotificationAction::activated, this, &StatusNotifier::sentryActivated);
0122         } else {
0123             auto action = notification->addAction(i18nc("Notification action button, keep short", "Report Bug"));
0124             connect(action, &KNotificationAction::activated, this, &StatusNotifier::activated);
0125         }
0126     }
0127     if (canBeRestarted(crashedApp)) {
0128         auto action = notification->addAction(i18nc("Notification action button, keep short", "Restart App"));
0129         connect(action, &KNotificationAction::activated, this, [crashedApp]() {
0130             if (canBeRestarted(crashedApp)) {
0131                 crashedApp->restart();
0132             }
0133         });
0134     }
0135 
0136     // when the SNI disappears you won't be able to interact with the notification anymore anyway, so close it
0137     if (activationAllowed) {
0138         connect(this, &StatusNotifier::activated, notification, &KNotification::close);
0139         connect(this, &StatusNotifier::expired, notification, &KNotification::close);
0140     } else {
0141         // No SNI means we should quit when the notification is gone
0142         connect(notification, &KNotification::closed, this, &StatusNotifier::expired);
0143     }
0144 }
0145 
0146 bool StatusNotifier::notificationServiceRegistered()
0147 {
0148     return QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.Notifications"));
0149 }
0150 
0151 bool StatusNotifier::canBeRestarted(CrashedApplication *app)
0152 {
0153     return !app->hasBeenRestarted() && app->fakeExecutableBaseName() != QLatin1String("drkonqi");
0154 }
0155 
0156 #include "moc_statusnotifier.cpp"