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"