diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..259148f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c1a2629 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.5) + +project(KDiskInfo VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +set(PROJECT_SOURCES + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui +) + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(KDiskInfo + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + mainwindow.ui + statusdot.h statusdot.cpp + custombutton.h custombutton.cpp + ) +# Define target properties for Android with Qt 6 as: +# set_property(TARGET KDiskInfo APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR +# ${CMAKE_CURRENT_SOURCE_DIR}/android) +# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation +else() + if(ANDROID) + add_library(KDiskInfo SHARED + ${PROJECT_SOURCES} + ) +# Define properties for Android with Qt 5 after find_package() calls as: +# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") + else() + add_executable(KDiskInfo + ${PROJECT_SOURCES} + ) + endif() +endif() + +target_link_libraries(KDiskInfo PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manually though. +if(${QT_VERSION} VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.KDiskInfo) +endif() +set_target_properties(KDiskInfo PROPERTIES + ${BUNDLE_ID_OPTION} + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +include(GNUInstallDirs) +install(TARGETS KDiskInfo + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(KDiskInfo) +endif() diff --git a/custombutton.cpp b/custombutton.cpp new file mode 100644 index 0000000..920d3b2 --- /dev/null +++ b/custombutton.cpp @@ -0,0 +1,22 @@ +#include "custombutton.h" + +CustomButton::CustomButton(const QString &text1, const QString &text2, const QString &text3, const QColor &lineColor, QWidget *parent) + : QPushButton(parent), text1(text1), text2(text2), text3(text3), lineColor(lineColor) { + setMinimumHeight(60); + setMinimumWidth(100); +} + +void CustomButton::paintEvent(QPaintEvent *event) { + QPushButton::paintEvent(event); + QPainter painter(this); + QPalette pal = palette(); + QColor textColor = pal.color(QPalette::ButtonText); + + QPen pen(lineColor, 5); + painter.setPen(pen); + painter.drawLine(10, 9, 10, 50); + + QRect rect(20, 0, width() - 25, height()); + painter.setPen(textColor); + painter.drawText(rect, Qt::AlignVCenter | Qt::AlignLeft, text1 + "\n" + text2 + "\n" + text3); +} diff --git a/custombutton.h b/custombutton.h new file mode 100644 index 0000000..8dd7e98 --- /dev/null +++ b/custombutton.h @@ -0,0 +1,23 @@ +#ifndef CUSTOMBUTTON_H +#define CUSTOMBUTTON_H + +#include +#include + +class CustomButton : public QPushButton { + Q_OBJECT + +public: + CustomButton(const QString &text1, const QString &text2, const QString &text3, const QColor &lineColor, QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString text1; + QString text2; + QString text3; + QColor lineColor; +}; + +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..fd3e533 --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include "mainwindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..3d095f4 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,348 @@ +#include "mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + buttonGroup = new QButtonGroup(this); + buttonGroup->setExclusive(true); + + horizontalLayout = ui->horizontalLayout; + + diskName = qobject_cast(ui->centralwidget->findChild("diskName")); + temperatureValue = qobject_cast(ui->centralwidget->findChild("temperatureValueLabel")); + healthStatusValue = qobject_cast(ui->centralwidget->findChild("healthStatusValueLabel")); + + firmwareLineEdit = qobject_cast(ui->centralwidget->findChild("firmwareLineEdit")); + serialNumberLineEdit = qobject_cast(ui->centralwidget->findChild("serialNumberLineEdit")); + typeLineEdit = qobject_cast(ui->centralwidget->findChild("typeLineEdit")); + protocolLineEdit = qobject_cast(ui->centralwidget->findChild("protocolLineEdit")); + deviceNodeLineEdit = qobject_cast(ui->centralwidget->findChild("deviceNodeLineEdit")); + + totalReadsLineEdit = qobject_cast(ui->centralwidget->findChild("totalReadsLineEdit")); + totalWritesLineEdit = qobject_cast(ui->centralwidget->findChild("totalWritesLineEdit")); + rotationRateLineEdit = qobject_cast(ui->centralwidget->findChild("rotationRateLineEdit")); + powerOnCountLineEdit = qobject_cast(ui->centralwidget->findChild("powerOnCountLineEdit")); + powerOnHoursLineEdit = qobject_cast(ui->centralwidget->findChild("powerOnHoursLineEdit")); + + tableWidget = qobject_cast(ui->centralwidget->findChild("dataTable"));; + serialNumberLineEdit->setEchoMode(QLineEdit::Password); + + QAction *toggleEchoModeAction = serialNumberLineEdit->addAction(QIcon::fromTheme(QStringLiteral("visibility")), QLineEdit::TrailingPosition); + connect(toggleEchoModeAction, &QAction::triggered, [=]() { + if (serialNumberLineEdit->echoMode() == QLineEdit::Password) { + serialNumberLineEdit->setEchoMode(QLineEdit::Normal); + toggleEchoModeAction->setIcon(QIcon::fromTheme(QStringLiteral("hint"))); + } else if (serialNumberLineEdit->echoMode() == QLineEdit::Normal) { + serialNumberLineEdit->setEchoMode(QLineEdit::Password); + toggleEchoModeAction->setIcon(QIcon::fromTheme(QStringLiteral("visibility"))); + } + }); + + scanDevices(); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::scanDevices() +{ + QString output = getSmartctlOutput({"--scan", "--json"}, false); + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); + QJsonObject jsonObj = doc.object(); + QJsonArray devices = jsonObj["devices"].toArray(); + QJsonObject globalObj; + QString globalHealth; + bool firstTime = true; + + for (const QJsonValue &value : devices) { + QJsonObject device = value.toObject(); + QString deviceName = device["name"].toString(); + + QString allOutput = getSmartctlOutput({"--all", "--json", deviceName}, true); + + QJsonDocument localDoc = QJsonDocument::fromJson(allOutput.toUtf8()); + QJsonObject localObj = localDoc.object(); + + QJsonArray attributes = localObj["ata_smart_attributes"].toObject()["table"].toArray(); + QString temperature = "N/A"; + bool healthPassed = localObj["smart_status"].toObject()["passed"].toBool(); + bool caution = false; + QString health; + QColor healthColor; + + bool isNvme = false; + QString protocol = localObj["device"].toObject()["protocol"].toString(); + if (protocol == "NVMe") { + isNvme = true; + } + + for (const QJsonValue &attr : attributes) { // Need different logic for NVMe + QJsonObject attrObj = attr.toObject(); + if (attrObj["id"] == 194 && !isNvme) { + QString raw = attrObj["raw"].toObject()["string"].toString(); + int spaceIndex = raw.indexOf(' '); + if (spaceIndex != -1) { + raw = raw.left(spaceIndex); + } + int tempInt = raw.toInt(); + temperature = QString::number(tempInt) + " °C"; + } else if (!isNvme && (attrObj["id"] == 5 || attrObj["id"] == 197 || attrObj["id"] == 198) && (attrObj["raw"].toObject()["value"].toInt() != 0)) { + caution = true; + } + } + + if (healthPassed && !caution) { + health = "Good"; + healthColor = Qt::green; + } else if (healthPassed && caution) { + health = "Caution"; + healthColor = Qt::yellow; + } else { + health = "Bad"; + healthColor = Qt::red; + } + + CustomButton *button = new CustomButton(health, deviceName, temperature, healthColor, this); + + buttonGroup->addButton(button); + horizontalLayout->addWidget(button); + + button->setCheckable(true); + button->setAutoExclusive(true); + + connect(button, &QPushButton::clicked, this, [=]() { + populateWindow(localObj, health); + }); + + if (firstTime) { + globalObj = localObj; + globalHealth = health; + button->setChecked(true); + } + + firstTime = false; + } + horizontalLayout->addStretch(); + populateWindow(globalObj, globalHealth); +} + +void MainWindow::populateWindow(const QJsonObject &localObj, const QString &health) +{ + QJsonArray attributes = localObj["ata_smart_attributes"].toObject()["table"].toArray(); + QString modelName = localObj["model_name"].toString(); + QString firmwareVersion = localObj["firmware_version"].toString(); + float userCapacityGB = localObj.value("user_capacity").toObject().value("bytes").toDouble() / 1e9; + QString userCapacityString = QString::number(static_cast(userCapacityGB)) + "." + QString::number(static_cast((userCapacityGB - static_cast(userCapacityGB)) * 10)) + " GB"; + QString totalReads = "----"; + QString totalWrites = "----"; + QString serialNumber = localObj["serial_number"].toString(); + QJsonObject deviceObj = localObj["device"].toObject(); + QString protocol = deviceObj["protocol"].toString(); + QString type = deviceObj["type"].toString(); + QString name = deviceObj["name"].toString(); + int tempInt = 0; + + bool isNvme = false; + if (protocol == "NVMe") { + isNvme = true; + } + + diskName->setText("

" + modelName + " " + userCapacityString + "

"); + firmwareLineEdit->setText(firmwareVersion); + serialNumberLineEdit->setText(serialNumber); + typeLineEdit->setText(type); + protocolLineEdit->setText(protocol); + deviceNodeLineEdit->setText(name); + + int rotationRateInt = localObj["rotation_rate"].toInt(-1); + QString rotationRate; + if (rotationRateInt > 0) { + rotationRate = QString::number(rotationRateInt); + } else if (rotationRateInt == 0) { + rotationRate = "---- (SSD)"; + } else { + rotationRate = "----"; + } + + rotationRateLineEdit->setText(rotationRate); + rotationRateLineEdit->setAlignment(Qt::AlignRight); + + int powerCycleCountInt = localObj["power_cycle_count"].toInt(-1); + QString powerCycleCount; + if (powerCycleCountInt >= 0) { + powerCycleCount = QString::number(powerCycleCountInt) + " count"; + } else { + powerCycleCount = "Unknown"; + } + + powerOnCountLineEdit->setText(powerCycleCount); + powerOnCountLineEdit->setAlignment(Qt::AlignRight); + + int powerOnTimeInt = localObj["power_on_time"].toObject().value("hours").toInt(-1); + QString powerOnTime; + if (powerOnTimeInt >= 0) { + powerOnTime = QString::number(powerOnTimeInt) + " hours"; + } else { + powerOnTime = "Unknown"; + } + + powerOnHoursLineEdit->setText(powerOnTime); + powerOnHoursLineEdit->setAlignment(Qt::AlignRight); + + for (const QJsonValue &attr : attributes) { //Need different logic for NVMe + QJsonObject attrObj = attr.toObject(); + if (attrObj["id"] == 241 && !isNvme) { + totalWrites = QString::number(attrObj["raw"].toObject()["value"].toInt()) + " GB"; + } else if (attrObj["id"] == 242 && !isNvme) { + totalReads = QString::number(attrObj["raw"].toObject()["value"].toInt()) + " GB"; + } else if (attrObj["id"] == 194 && !isNvme) { + QString raw = attrObj["raw"].toObject()["string"].toString(); + int spaceIndex = raw.indexOf(' '); + if (spaceIndex != -1) { + raw = raw.left(spaceIndex); + } + tempInt = raw.toInt(); + } + } + + totalReadsLineEdit->setText(totalReads); + totalReadsLineEdit->setAlignment(Qt::AlignRight); + + totalWritesLineEdit->setText(totalWrites); + totalWritesLineEdit->setAlignment(Qt::AlignRight); + + if (tempInt > 55) { + temperatureValue->setStyleSheet("background-color: " + QColor(Qt::red).name() + ";"); + } else if ((tempInt < 55) && (tempInt > 50)){ + temperatureValue->setStyleSheet("background-color: " + QColor(Qt::yellow).name() + ";"); + } else if (tempInt == 0) { + temperatureValue->setStyleSheet("background-color: " + QColor(Qt::gray).name() + ";"); + } else { + temperatureValue->setStyleSheet("background-color: " + QColor(Qt::green).name() + ";"); + } + + QString labelStyle = "font-size:12pt; font-weight:700; color:black"; + + if (tempInt > 0) { + temperatureValue->setText("

" + QString::number(tempInt) + " °C

"); + } else { + temperatureValue->setText("

N/A

"); + } + + temperatureValue->setAlignment(Qt::AlignCenter); + + if (health == "Bad") { + healthStatusValue->setStyleSheet("background-color: " + QColor(Qt::red).name() + ";"); + } else if (health == "Caution"){ + healthStatusValue->setStyleSheet("background-color: " + QColor(Qt::yellow).name() + ";"); + } else { + healthStatusValue->setStyleSheet("background-color: " + QColor(Qt::green).name() + ";"); + } + + healthStatusValue->setText("

" + health + "

"); + healthStatusValue->setAlignment(Qt::AlignCenter); + + if (protocol != "NVMe") { + addSmartAttributesTable(attributes); + } +} + +void MainWindow::addSmartAttributesTable(const QJsonArray &attributes) +{ + tableWidget->setColumnCount(7); + tableWidget->setHorizontalHeaderLabels({"", "ID", "Attribute Name", "Current", "Worst", "Threshold", "Raw Values"}); + tableWidget->verticalHeader()->setVisible(false); + tableWidget->setItemDelegateForColumn(0, new StatusDot(tableWidget)); + tableWidget->setRowCount(attributes.size()); + + int row = 0; + for (const QJsonValue &attr : attributes) { + QJsonObject attrObj = attr.toObject(); + QString id = QString("%1").arg(attrObj["id"].toInt(), 2, 16, QChar('0')).toUpper(); + QString name = attrObj["name"].toString().replace("_", " "); + int value = attrObj["value"].toInt(); + int worst = attrObj["worst"].toInt(); + int thresh = attrObj["thresh"].toInt(); + QString raw = attrObj["raw"].toObject()["string"].toString(); + + int spaceIndex = raw.indexOf(' '); + if (spaceIndex != -1) { + raw = raw.left(spaceIndex); + } + raw = QString("%1").arg(raw.toUInt(nullptr), 12, 16, QChar('0')).toUpper(); + + QColor statusColor; + if ((thresh != 0) && (value < thresh)) { + statusColor = Qt::red; + } else if ((id == "05" || id == "C5" || id == "C6") && (raw != "0")) { + statusColor = Qt::yellow; + } else { + statusColor = Qt::green; + } + + QTableWidgetItem *statusItem = new QTableWidgetItem(); + statusItem->setBackground(Qt::transparent); + statusItem->setData(Qt::BackgroundRole, QVariant(statusColor)); + + QTableWidgetItem *idItem = new QTableWidgetItem(id); + idItem->setTextAlignment(Qt::AlignCenter); + + QTableWidgetItem *nameItem = new QTableWidgetItem(name); + QTableWidgetItem *valueItem = new QTableWidgetItem(QString::number(value)); + valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QTableWidgetItem *worstItem = new QTableWidgetItem(QString::number(worst)); + worstItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QTableWidgetItem *threshItem = new QTableWidgetItem(QString::number(thresh)); + threshItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QTableWidgetItem *rawItem = new QTableWidgetItem(raw); + rawItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + + tableWidget->setItem(row, 0, statusItem); + tableWidget->setItem(row, 1, idItem); + tableWidget->setItem(row, 2, nameItem); + tableWidget->setItem(row, 3, valueItem); + tableWidget->setItem(row, 4, worstItem); + tableWidget->setItem(row, 5, threshItem); + tableWidget->setItem(row, 6, rawItem); + + ++row; + } + + tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + for (int i = 0; i < tableWidget->columnCount(); ++i) { + if (i != 2) { + tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + } + + tableWidget->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); + tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); +} + +QString MainWindow::getSmartctlOutput(const QStringList &arguments, bool root) +{ + QProcess process; + QString command; + QStringList commandArgs; + if(root) { + command = "pkexec"; + commandArgs = {"smartctl"}; + } else { + command = "smartctl"; + commandArgs = {}; + } + + commandArgs.append(arguments); + process.start(command, commandArgs); + process.waitForFinished(); + return process.readAllStandardOutput(); +} + diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..a829e33 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,44 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +#include "statusdot.h" +#include "custombutton.h" +#include "./ui_mainwindow.h" + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; + QButtonGroup *buttonGroup; + QHBoxLayout *horizontalLayout; + QLabel *diskName, *temperatureValue, *healthStatusValue; + QLineEdit *firmwareLineEdit, *serialNumberLineEdit, *typeLineEdit, *protocolLineEdit, *deviceNodeLineEdit; + QLineEdit *totalReadsLineEdit, *totalWritesLineEdit, *rotationRateLineEdit, *powerOnCountLineEdit, *powerOnHoursLineEdit; + QTableWidget *tableWidget; + + void scanDevices(); + void populateWindow(const QJsonObject &tempObj, const QString &health); + void addSmartAttributesTable(const QJsonArray &attributes); + QString getSmartctlOutput(const QStringList &arguments, bool root); +}; +#endif diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..85cee04 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,326 @@ + + + MainWindow + + + Qt::WindowModality::NonModal + + + + 0 + 0 + 800 + 600 + + + + + 0 + 0 + + + + KDiskInfo + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + <html><head/><body><p align="center"><span style=" font-size:14pt; font-weight:700;">Hard Drive Name</span></p></body></html> + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + + <html><head/><body><p align="center">Health Status</p></body></html> + + + + + + + background-color: rgb(0, 255, 0); + + + <html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:700; color:#000000;">Good</span></p></body></html> + + + + + + + <html><head/><body><p align="center">Temperature</p></body></html> + + + + + + + + 16777215 + 50 + + + + background-color: rgb(0, 255, 0); + + + <html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:700; color:#000000;">23° C</span></p></body></html> + + + + + + + + + + + + + Firmware + + + + + + + true + + + + + + + Serial Number + + + + + + + true + + + + + + + Protocol + + + + + + + true + + + + + + + Device Node + + + + + + + true + + + + + + true + + + + + + + Type + + + + + + + true + + + + + + + + + + + + + + + Total Host Reads + + + + + + + true + + + + + + + Total Host Writes + + + + + + + true + + + + + + + Rotation Rate + + + + + + + true + + + + + + + Power On Count + + + + + + + true + + + + + + + Power On Hours + + + + + + + true + + + + + + + + + + + + + false + + + QFrame::Shadow::Raised + + + true + + + true + + + + + + + + + + + diff --git a/statusdot.cpp b/statusdot.cpp new file mode 100644 index 0000000..0921c5e --- /dev/null +++ b/statusdot.cpp @@ -0,0 +1,17 @@ +#include "statusdot.h" + +void StatusDot::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + int dotSize = qMin(opt.rect.width(), opt.rect.height()) * 0.5; + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + QRect dotRect(opt.rect.center().x() - dotSize / 2, opt.rect.center().y() - dotSize / 2, dotSize, dotSize); + QColor color = QColor(index.data(Qt::BackgroundRole).value()); + painter->setBrush(color); + painter->setPen(Qt::NoPen); + painter->drawEllipse(dotRect); + painter->restore(); +} diff --git a/statusdot.h b/statusdot.h new file mode 100644 index 0000000..572a2d3 --- /dev/null +++ b/statusdot.h @@ -0,0 +1,20 @@ +#ifndef STATUSDOT_H +#define STATUSDOT_H + +#include +#include +#include + +class StatusDot : public QStyledItemDelegate +{ +public: + StatusDot(QObject *parent = nullptr) + : QStyledItemDelegate(parent) + {} + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +#endif