File indexing completed on 2024-06-23 05:14:06

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     dialogs/selftestdialog.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-kleopatra.h>
0011 
0012 #include "selftestdialog.h"
0013 
0014 #include <selftest/selftest.h>
0015 #include <utils/accessibility.h>
0016 #include <utils/scrollarea.h>
0017 
0018 #include <Libkleo/SystemInfo>
0019 #include <Libkleo/TreeView>
0020 
0021 #include <KColorScheme>
0022 #include <KLocalizedString>
0023 
0024 #include <QAbstractTableModel>
0025 #include <QApplication>
0026 #include <QCheckBox>
0027 #include <QDialogButtonBox>
0028 #include <QGroupBox>
0029 #include <QHBoxLayout>
0030 #include <QHeaderView>
0031 #include <QLabel>
0032 #include <QPushButton>
0033 #include <QSortFilterProxyModel>
0034 #include <QSplitter>
0035 #include <QVBoxLayout>
0036 
0037 #include "kleopatra_debug.h"
0038 
0039 using namespace Kleo;
0040 using namespace Kleo::Dialogs;
0041 
0042 namespace
0043 {
0044 
0045 class Model : public QAbstractTableModel
0046 {
0047     Q_OBJECT
0048 public:
0049     explicit Model(QObject *parent = nullptr)
0050         : QAbstractTableModel(parent)
0051         , m_tests()
0052     {
0053     }
0054 
0055     enum Column {
0056         TestName,
0057         TestResult,
0058 
0059         NumColumns
0060     };
0061 
0062     const std::shared_ptr<SelfTest> &fromModelIndex(const QModelIndex &idx) const
0063     {
0064         const unsigned int row = idx.row();
0065         if (row < m_tests.size()) {
0066             return m_tests[row];
0067         }
0068         static const std::shared_ptr<SelfTest> null;
0069         return null;
0070     }
0071 
0072     int rowCount(const QModelIndex &idx) const override
0073     {
0074         return idx.isValid() ? 0 : m_tests.size();
0075     }
0076     int columnCount(const QModelIndex &) const override
0077     {
0078         return NumColumns;
0079     }
0080 
0081     QVariant data(const QModelIndex &idx, int role) const override
0082     {
0083         const unsigned int row = idx.row();
0084         if (idx.isValid() && row < m_tests.size())
0085             switch (role) {
0086             case Qt::DisplayRole:
0087             case Qt::ToolTipRole:
0088                 switch (idx.column()) {
0089                 case TestName:
0090                     return m_tests[row]->name();
0091                 case TestResult:
0092                     return m_tests[row]->skipped() ? i18n("Skipped") //
0093                         : m_tests[row]->passed()   ? i18n("Passed")
0094                                                    : m_tests[row]->shortError();
0095                 }
0096                 break;
0097             case Qt::BackgroundRole:
0098                 if (!SystemInfo::isHighContrastModeActive()) {
0099                     KColorScheme scheme(qApp->palette().currentColorGroup());
0100                     return (m_tests[row]->skipped()      ? scheme.background(KColorScheme::NeutralBackground)
0101                                 : m_tests[row]->passed() ? scheme.background(KColorScheme::PositiveBackground)
0102                                                          : scheme.background(KColorScheme::NegativeBackground))
0103                         .color();
0104                 }
0105             }
0106         return QVariant();
0107     }
0108 
0109     QVariant headerData(int section, Qt::Orientation o, int role) const override
0110     {
0111         if (o == Qt::Horizontal && section >= 0 && section < NumColumns && role == Qt::DisplayRole)
0112             switch (section) {
0113             case TestName:
0114                 return i18n("Test Name");
0115             case TestResult:
0116                 return i18n("Result");
0117             }
0118         return QVariant();
0119     }
0120 
0121     void clear()
0122     {
0123         if (m_tests.empty()) {
0124             return;
0125         }
0126         beginRemoveRows(QModelIndex(), 0, m_tests.size() - 1);
0127         m_tests.clear();
0128         endRemoveRows();
0129     }
0130 
0131     void append(const std::vector<std::shared_ptr<SelfTest>> &tests)
0132     {
0133         if (tests.empty()) {
0134             return;
0135         }
0136         beginInsertRows(QModelIndex(), m_tests.size(), m_tests.size() + tests.size());
0137         m_tests.insert(m_tests.end(), tests.begin(), tests.end());
0138         endInsertRows();
0139     }
0140 
0141     void reloadData()
0142     {
0143         if (!m_tests.empty()) {
0144             Q_EMIT dataChanged(index(0, 0), index(m_tests.size() - 1, NumColumns - 1));
0145         }
0146     }
0147 
0148     const std::shared_ptr<SelfTest> &at(unsigned int idx) const
0149     {
0150         return m_tests.at(idx);
0151     }
0152 
0153 private:
0154     std::vector<std::shared_ptr<SelfTest>> m_tests;
0155 };
0156 
0157 class Proxy : public QSortFilterProxyModel
0158 {
0159     Q_OBJECT
0160 public:
0161     explicit Proxy(QObject *parent = nullptr)
0162         : QSortFilterProxyModel(parent)
0163         , m_showAll(true)
0164     {
0165         setDynamicSortFilter(true);
0166     }
0167 
0168     bool showAll() const
0169     {
0170         return m_showAll;
0171     }
0172 
0173 Q_SIGNALS:
0174     void showAllChanged(bool);
0175 
0176 public Q_SLOTS:
0177     void setShowAll(bool on)
0178     {
0179         if (on == m_showAll) {
0180             return;
0181         }
0182         m_showAll = on;
0183         invalidateFilter();
0184         Q_EMIT showAllChanged(on);
0185     }
0186 
0187 private:
0188     bool filterAcceptsRow(int src_row, const QModelIndex &src_parent) const override
0189     {
0190         if (m_showAll) {
0191             return true;
0192         }
0193         if (const Model *const model = qobject_cast<Model *>(sourceModel())) {
0194             if (!src_parent.isValid() && src_row >= 0 && src_row < model->rowCount(src_parent)) {
0195                 if (const std::shared_ptr<SelfTest> &t = model->at(src_row)) {
0196                     return !t->passed();
0197                 } else {
0198                     qCWarning(KLEOPATRA_LOG) << "NULL test??";
0199                 }
0200             } else {
0201                 if (src_parent.isValid()) {
0202                     qCWarning(KLEOPATRA_LOG) << "view asks for subitems!";
0203                 } else {
0204                     qCWarning(KLEOPATRA_LOG) << "index " << src_row << " is out of range [" << 0 << "," << model->rowCount(src_parent) << "]";
0205                 }
0206             }
0207         } else {
0208             qCWarning(KLEOPATRA_LOG) << "expected a ::Model, got ";
0209             if (!sourceModel()) {
0210                 qCWarning(KLEOPATRA_LOG) << "a null pointer";
0211             } else {
0212                 qCWarning(KLEOPATRA_LOG) << sourceModel()->metaObject()->className();
0213             }
0214         }
0215         return false;
0216     }
0217 
0218 private:
0219     bool m_showAll;
0220 };
0221 
0222 class TreeViewInternal : public TreeView
0223 {
0224     Q_OBJECT
0225 public:
0226     using TreeView::TreeView;
0227 
0228 protected:
0229     void focusInEvent(QFocusEvent *event) override
0230     {
0231         TreeView::focusInEvent(event);
0232         // queue the invokation, so that it happens after the widget itself got focus
0233         QMetaObject::invokeMethod(this, &TreeViewInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection);
0234     }
0235 
0236 private:
0237     void forceAccessibleFocusEventForCurrentItem()
0238     {
0239         // force Qt to send a focus event for the current item to accessibility
0240         // tools; otherwise, the user has no idea which item is selected when the
0241         // list gets keyboard input focus
0242         const auto current = currentIndex();
0243         setCurrentIndex({});
0244         setCurrentIndex(current);
0245     }
0246 };
0247 
0248 }
0249 
0250 class SelfTestDialog::Private
0251 {
0252     friend class ::Kleo::Dialogs::SelfTestDialog;
0253     SelfTestDialog *const q;
0254 
0255 public:
0256     explicit Private(SelfTestDialog *qq)
0257         : q(qq)
0258         , model(q)
0259         , proxy(q)
0260         , ui(q)
0261     {
0262         proxy.setSourceModel(&model);
0263         ui.resultsTV->setModel(&proxy);
0264 
0265         ui.detailsGB->hide();
0266         ui.proposedCorrectiveActionGB->hide();
0267 
0268         connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept);
0269         connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
0270         connect(ui.doItPB, &QAbstractButton::clicked, q, [this]() {
0271             slotDoItClicked();
0272         });
0273         connect(ui.rerunPB, &QAbstractButton::clicked, q, &SelfTestDialog::updateRequested);
0274         connect(ui.resultsTV->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() {
0275             slotSelectionChanged();
0276         });
0277         connect(ui.showAllCB, &QAbstractButton::toggled, q, [this](bool checked) {
0278             proxy.setShowAll(checked);
0279             if (checked) {
0280                 updateColumnSizes();
0281             }
0282             ensureCurrentItemIsVisible();
0283         });
0284         proxy.setShowAll(ui.showAllCB->isChecked());
0285 
0286         ui.resultsTV->setFocus();
0287     }
0288 
0289 private:
0290     void slotSelectionChanged()
0291     {
0292         const int row = selectedRowIndex();
0293         if (row < 0) {
0294             ui.detailsLB->setText(i18n("(select test first)"));
0295             ui.detailsGB->hide();
0296             ui.proposedCorrectiveActionGB->hide();
0297         } else {
0298             const std::shared_ptr<SelfTest> &t = model.at(row);
0299             ui.detailsLB->setText(t->longError());
0300             ui.detailsGB->setVisible(!t->passed());
0301             const QString action = t->proposedFix();
0302             ui.proposedCorrectiveActionGB->setVisible(!t->passed() && !action.isEmpty());
0303             ui.proposedCorrectiveActionLB->setText(action);
0304             ui.doItPB->setVisible(!t->passed() && t->canFixAutomatically());
0305             QMetaObject::invokeMethod(
0306                 q,
0307                 [this]() {
0308                     ensureCurrentItemIsVisible();
0309                 },
0310                 Qt::QueuedConnection);
0311         }
0312     }
0313     void slotDoItClicked()
0314     {
0315         if (const std::shared_ptr<SelfTest> st = model.fromModelIndex(selectedRow()))
0316             if (st->fix()) {
0317                 model.reloadData();
0318             }
0319     }
0320 
0321 private:
0322     void ensureCurrentItemIsVisible()
0323     {
0324         ui.resultsTV->scrollTo(ui.resultsTV->currentIndex());
0325     }
0326     void updateColumnSizes()
0327     {
0328         ui.resultsTV->header()->resizeSections(QHeaderView::ResizeToContents);
0329     }
0330 
0331 private:
0332     QModelIndex selectedRow() const
0333     {
0334         const QItemSelectionModel *const ism = ui.resultsTV->selectionModel();
0335         if (!ism) {
0336             return QModelIndex();
0337         }
0338         const QModelIndexList mil = ism->selectedRows();
0339         return mil.empty() ? QModelIndex() : proxy.mapToSource(mil.front());
0340     }
0341     int selectedRowIndex() const
0342     {
0343         return selectedRow().row();
0344     }
0345 
0346 private:
0347     Model model;
0348     Proxy proxy;
0349 
0350     struct UI {
0351         TreeViewInternal *resultsTV = nullptr;
0352         QCheckBox *showAllCB = nullptr;
0353         QGroupBox *detailsGB = nullptr;
0354         QLabel *detailsLB = nullptr;
0355         QGroupBox *proposedCorrectiveActionGB = nullptr;
0356         QLabel *proposedCorrectiveActionLB = nullptr;
0357         QPushButton *doItPB = nullptr;
0358         QCheckBox *runAtStartUpCB;
0359         QDialogButtonBox *buttonBox;
0360         QPushButton *rerunPB = nullptr;
0361 
0362         LabelHelper labelHelper;
0363 
0364         explicit UI(SelfTestDialog *qq)
0365         {
0366             auto mainLayout = new QVBoxLayout{qq};
0367 
0368             {
0369                 auto label = new QLabel{xi18n("<para>These are the results of the Kleopatra self-test suite. Click on a test for details.</para>"
0370                                               "<para>Note that all but the first failure might be due to prior tests failing.</para>"),
0371                                         qq};
0372                 label->setWordWrap(true);
0373                 labelHelper.addLabel(label);
0374 
0375                 mainLayout->addWidget(label);
0376             }
0377 
0378             auto splitter = new QSplitter{qq};
0379             splitter->setOrientation(Qt::Vertical);
0380 
0381             {
0382                 auto widget = new QWidget{qq};
0383                 auto vbox = new QVBoxLayout{widget};
0384                 vbox->setContentsMargins(0, 0, 0, 0);
0385 
0386                 resultsTV = new TreeViewInternal{qq};
0387                 resultsTV->setAccessibleName(i18n("test results"));
0388                 QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0389                 sizePolicy.setHorizontalStretch(0);
0390                 sizePolicy.setVerticalStretch(1);
0391                 sizePolicy.setHeightForWidth(resultsTV->sizePolicy().hasHeightForWidth());
0392                 resultsTV->setSizePolicy(sizePolicy);
0393                 resultsTV->setMinimumHeight(100);
0394                 resultsTV->setRootIsDecorated(false);
0395                 resultsTV->setAllColumnsShowFocus(true);
0396                 vbox->addWidget(resultsTV);
0397 
0398                 splitter->addWidget(widget);
0399             }
0400             {
0401                 detailsGB = new QGroupBox{i18nc("@title:group", "Details"), qq};
0402                 auto groupBoxLayout = new QVBoxLayout{detailsGB};
0403 
0404                 auto scrollArea = new Kleo::ScrollArea{qq};
0405                 scrollArea->setFocusPolicy(Qt::NoFocus);
0406                 scrollArea->setMinimumHeight(100);
0407                 scrollArea->setFrameShape(QFrame::NoFrame);
0408                 scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0409                 auto scrollAreaLayout = qobject_cast<QBoxLayout *>(scrollArea->widget()->layout());
0410 
0411                 detailsLB = new QLabel{qq};
0412                 detailsLB->setTextFormat(Qt::RichText);
0413                 detailsLB->setTextInteractionFlags(Qt::TextSelectableByMouse);
0414                 detailsLB->setWordWrap(true);
0415                 labelHelper.addLabel(detailsLB);
0416 
0417                 scrollAreaLayout->addWidget(detailsLB);
0418                 scrollAreaLayout->addStretch();
0419 
0420                 groupBoxLayout->addWidget(scrollArea);
0421 
0422                 splitter->addWidget(detailsGB);
0423             }
0424             {
0425                 proposedCorrectiveActionGB = new QGroupBox{i18nc("@title:group", "Proposed Corrective Action"), qq};
0426                 auto groupBoxLayout = new QVBoxLayout{proposedCorrectiveActionGB};
0427 
0428                 auto scrollArea = new Kleo::ScrollArea{qq};
0429                 scrollArea->setFocusPolicy(Qt::NoFocus);
0430                 scrollArea->setMinimumHeight(100);
0431                 scrollArea->setFrameShape(QFrame::NoFrame);
0432                 scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0433                 auto scrollAreaLayout = qobject_cast<QBoxLayout *>(scrollArea->widget()->layout());
0434 
0435                 proposedCorrectiveActionLB = new QLabel{qq};
0436                 proposedCorrectiveActionLB->setTextFormat(Qt::RichText);
0437                 proposedCorrectiveActionLB->setTextInteractionFlags(Qt::TextSelectableByMouse);
0438                 proposedCorrectiveActionLB->setWordWrap(true);
0439                 labelHelper.addLabel(proposedCorrectiveActionLB);
0440 
0441                 scrollAreaLayout->addWidget(proposedCorrectiveActionLB);
0442                 scrollAreaLayout->addStretch();
0443 
0444                 groupBoxLayout->addWidget(scrollArea);
0445 
0446                 {
0447                     auto hbox = new QHBoxLayout;
0448                     hbox->addStretch();
0449 
0450                     doItPB = new QPushButton{i18nc("@action:button", "Do It"), qq};
0451                     doItPB->setEnabled(false);
0452                     hbox->addWidget(doItPB);
0453 
0454                     groupBoxLayout->addLayout(hbox);
0455                 }
0456 
0457                 splitter->addWidget(proposedCorrectiveActionGB);
0458             }
0459 
0460             mainLayout->addWidget(splitter);
0461 
0462             showAllCB = new QCheckBox{i18nc("@option:check", "Show all test results"), qq};
0463             showAllCB->setChecked(true);
0464             mainLayout->addWidget(showAllCB);
0465 
0466             runAtStartUpCB = new QCheckBox{i18nc("@option:check", "Run these tests at startup"), qq};
0467             runAtStartUpCB->setChecked(true);
0468             mainLayout->addWidget(runAtStartUpCB);
0469 
0470             buttonBox = new QDialogButtonBox{qq};
0471             buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Close | QDialogButtonBox::Ok);
0472             buttonBox->button(QDialogButtonBox::Ok)->setText(i18nc("@action:button", "Continue"));
0473             rerunPB = buttonBox->addButton(i18nc("@action:button", "Rerun Tests"), QDialogButtonBox::ActionRole);
0474 
0475             mainLayout->addWidget(buttonBox);
0476         }
0477     } ui;
0478 };
0479 
0480 SelfTestDialog::SelfTestDialog(QWidget *p, Qt::WindowFlags f)
0481     : QDialog(p, f)
0482     , d(new Private(this))
0483 {
0484     setWindowTitle(i18nc("@title:window", "Self Test"));
0485     resize(448, 610);
0486 
0487     setAutomaticMode(false);
0488 }
0489 
0490 SelfTestDialog::~SelfTestDialog() = default;
0491 
0492 void SelfTestDialog::setTests(const std::vector<std::shared_ptr<SelfTest>> &tests)
0493 {
0494     d->model.clear();
0495     d->model.append(tests);
0496     d->updateColumnSizes();
0497 }
0498 
0499 void SelfTestDialog::setRunAtStartUp(bool on)
0500 {
0501     d->ui.runAtStartUpCB->setChecked(on);
0502 }
0503 
0504 bool SelfTestDialog::runAtStartUp() const
0505 {
0506     return d->ui.runAtStartUpCB->isChecked();
0507 }
0508 
0509 void SelfTestDialog::setAutomaticMode(bool automatic)
0510 {
0511     d->ui.showAllCB->setChecked(!automatic);
0512     d->ui.buttonBox->button(QDialogButtonBox::Ok)->setVisible(automatic);
0513     d->ui.buttonBox->button(QDialogButtonBox::Cancel)->setVisible(automatic);
0514     d->ui.buttonBox->button(QDialogButtonBox::Close)->setVisible(!automatic);
0515 }
0516 
0517 #include "moc_selftestdialog.cpp"
0518 #include "selftestdialog.moc"