File indexing completed on 2024-05-05 17:39:54
0001 /* 0002 * KSysGuard, the KDE System Guard 0003 * 0004 * SPDX-FileCopyrightText: 2022 Eugene Popov <popov895@ukr.net> 0005 * 0006 * SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "OpenFilesTab.h" 0010 0011 #include <QDir> 0012 #include <QGraphicsOpacityEffect> 0013 #include <QHeaderView> 0014 #include <QLabel> 0015 #include <QLayout> 0016 #include <QLineEdit> 0017 #include <QPushButton> 0018 #include <QSortFilterProxyModel> 0019 #include <QTimer> 0020 #include <QTreeView> 0021 0022 #include <KLocalizedString> 0023 #include <KMessageWidget> 0024 0025 #include <unistd.h> 0026 #include <sys/stat.h> 0027 0028 QString fileTypeFromPath(const QString &path) 0029 { 0030 struct stat statbuf; 0031 if (stat(qPrintable(path), &statbuf) == 0) { 0032 if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) { 0033 return i18nc("Device type", "File"); 0034 } 0035 if (S_ISCHR(statbuf.st_mode)) { 0036 return i18nc("Device type", "Character device"); 0037 } 0038 if (S_ISBLK(statbuf.st_mode)) { 0039 return i18nc("Device type", "Block device"); 0040 } 0041 if (S_ISFIFO(statbuf.st_mode)) { 0042 return i18nc("Device type", "Pipe"); 0043 } 0044 if (S_ISSOCK(statbuf.st_mode)) { 0045 return i18nc("Device type", "Socket"); 0046 } 0047 } 0048 0049 return QString(); 0050 } 0051 0052 QString symLinkTargetFromPath(const QString &path) 0053 { 0054 struct stat statbuf; 0055 if (lstat(qPrintable(path), &statbuf) == 0) { 0056 QVarLengthArray<char, 256> symLinkTarget(statbuf.st_size + 1); 0057 ssize_t count; 0058 if ((count = readlink(qPrintable(path), symLinkTarget.data(), symLinkTarget.size() - 1)) > 0) { 0059 symLinkTarget[count] = '\0'; 0060 0061 return symLinkTarget.constData(); 0062 } 0063 } 0064 0065 return QString(); 0066 } 0067 0068 class OpenFilesModel : public QAbstractTableModel 0069 { 0070 public: 0071 using QAbstractTableModel::QAbstractTableModel; 0072 using QAbstractTableModel::setData; 0073 0074 enum Column 0075 { 0076 Column_Id, 0077 Column_Type, 0078 Column_Filename, 0079 ColumnCount 0080 }; 0081 0082 using DataItem = struct 0083 { 0084 uint id; 0085 QString type; 0086 QString filename; 0087 }; 0088 using Data = QVector<DataItem>; 0089 0090 void setData(Data &&data) 0091 { 0092 beginResetModel(); 0093 m_data = std::forward<Data>(data); 0094 endResetModel(); 0095 } 0096 0097 QVariant data(const QModelIndex &index, int role) const override 0098 { 0099 if (index.isValid() && (role == Qt::DisplayRole || role == Qt::EditRole)) { 0100 const int row = index.row(); 0101 if (row >= 0 && row < m_data.count()) { 0102 const DataItem &dataItem = m_data.at(row); 0103 switch (index.column()) { 0104 case Column_Id: 0105 return dataItem.id; 0106 case Column_Type: 0107 return dataItem.type; 0108 case Column_Filename: 0109 return dataItem.filename; 0110 default: 0111 Q_UNREACHABLE(); 0112 } 0113 } 0114 } 0115 0116 return QVariant(); 0117 } 0118 0119 QVariant headerData(int section, Qt::Orientation orientation, int role) const override 0120 { 0121 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { 0122 switch (section) { 0123 case Column_Id: 0124 return i18nc("@title:column File ID", "Id"); 0125 case Column_Type: 0126 return i18nc("@title:column File type", "Type"); 0127 case Column_Filename: 0128 return i18nc("@title:column", "Filename"); 0129 default: 0130 Q_UNREACHABLE(); 0131 } 0132 } 0133 0134 return QVariant(); 0135 } 0136 0137 int columnCount(const QModelIndex &parent) const override 0138 { 0139 Q_UNUSED(parent); 0140 0141 return ColumnCount; 0142 } 0143 0144 int rowCount(const QModelIndex &parent) const override 0145 { 0146 Q_UNUSED(parent); 0147 0148 return m_data.count(); 0149 } 0150 0151 private: 0152 Data m_data; 0153 }; 0154 0155 OpenFilesTab::OpenFilesTab(QWidget *parent) 0156 : QWidget(parent) 0157 { 0158 m_errorWidget = new KMessageWidget; 0159 m_errorWidget->setCloseButtonVisible(false); 0160 m_errorWidget->setMessageType(KMessageWidget::Error); 0161 m_errorWidget->setWordWrap(true); 0162 m_errorWidget->hide(); 0163 0164 QPushButton *refreshButton = new QPushButton(i18nc("@action:button", "Refresh")); 0165 0166 m_searchEdit = new QLineEdit; 0167 m_searchEdit->setPlaceholderText(i18n("Quick search")); 0168 0169 m_dataModel = new OpenFilesModel(this); 0170 0171 m_proxyModel = new QSortFilterProxyModel(this); 0172 m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 0173 m_proxyModel->setFilterKeyColumn(-1); 0174 m_proxyModel->setSortRole(Qt::EditRole); 0175 m_proxyModel->setSourceModel(m_dataModel); 0176 0177 QTreeView *dataTreeView = new QTreeView; 0178 dataTreeView->setAlternatingRowColors(true); 0179 dataTreeView->setRootIsDecorated(false); 0180 dataTreeView->setSortingEnabled(true); 0181 dataTreeView->sortByColumn(OpenFilesModel::Column_Id, Qt::AscendingOrder); 0182 dataTreeView->setModel(m_proxyModel); 0183 dataTreeView->header()->setStretchLastSection(true); 0184 0185 QGridLayout *rootLayout = new QGridLayout; 0186 rootLayout->addWidget(m_errorWidget, 0, 0, 1, 2); 0187 rootLayout->addWidget(refreshButton, 1, 0); 0188 rootLayout->addWidget(m_searchEdit, 1, 1); 0189 rootLayout->addWidget(dataTreeView, 2, 0, 1, 2); 0190 setLayout(rootLayout); 0191 0192 m_placeholderLabel = new QLabel; 0193 m_placeholderLabel->setAlignment(Qt::AlignCenter); 0194 m_placeholderLabel->setMargin(20); 0195 m_placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction); 0196 m_placeholderLabel->setWordWrap(true); 0197 // To match the size of a level 2 Heading/KTitleWidget 0198 QFont placeholderFont = m_placeholderLabel->font(); 0199 placeholderFont.setPointSize(qRound(placeholderFont.pointSize() * 1.3)); 0200 m_placeholderLabel->setFont(placeholderFont); 0201 // Match opacity of QML placeholder label component 0202 QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(m_placeholderLabel); 0203 opacityEffect->setOpacity(0.5); 0204 m_placeholderLabel->setGraphicsEffect(opacityEffect); 0205 0206 QVBoxLayout *placeholderLayout = new QVBoxLayout; 0207 placeholderLayout->addWidget(m_placeholderLabel); 0208 dataTreeView->setLayout(placeholderLayout); 0209 0210 // use some delay while searching as you type, because an immediate 0211 // search in large data can slow down the UI 0212 QTimer *applySearchTimer = new QTimer(this); 0213 applySearchTimer->setInterval(350); 0214 applySearchTimer->setSingleShot(true); 0215 0216 connect(refreshButton, &QPushButton::clicked, this, &OpenFilesTab::refresh); 0217 0218 connect(m_searchEdit, &QLineEdit::textChanged, applySearchTimer, qOverload<>(&QTimer::start)); 0219 connect(m_searchEdit, &QLineEdit::editingFinished, applySearchTimer, &QTimer::stop); 0220 connect(m_searchEdit, &QLineEdit::editingFinished, this, &OpenFilesTab::onSearchEditEditingFinished); 0221 0222 connect(m_proxyModel, &QSortFilterProxyModel::modelReset, this, &OpenFilesTab::onProxyModelChanged); 0223 connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &OpenFilesTab::onProxyModelChanged); 0224 connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &OpenFilesTab::onProxyModelChanged); 0225 0226 connect(applySearchTimer, &QTimer::timeout, this, &OpenFilesTab::onSearchEditEditingFinished); 0227 } 0228 0229 void OpenFilesTab::setProcessId(long processId) 0230 { 0231 if (m_processId != processId) { 0232 m_processId = processId; 0233 refresh(); 0234 } 0235 } 0236 0237 void OpenFilesTab::refresh() 0238 { 0239 OpenFilesModel::Data data; 0240 0241 if (m_processId <= 0) { 0242 m_errorWidget->animatedHide(); 0243 } else { 0244 const QString dirPath = QStringLiteral("/proc/%1/fd").arg(m_processId); 0245 const QFileInfo dirFileInfo(dirPath); 0246 if (!dirFileInfo.exists() || !dirFileInfo.isReadable() || !dirFileInfo.isDir()) { 0247 m_errorWidget->setText(i18nc("@info:status", "%1: Failed to list directory contents", dirPath)); 0248 m_errorWidget->animatedShow(); 0249 } else { 0250 m_errorWidget->animatedHide(); 0251 0252 const QFileInfoList filesInfo = QDir(dirPath).entryInfoList(QDir::Files); 0253 for (const QFileInfo &fileInfo : filesInfo) { 0254 OpenFilesModel::DataItem dataItem; 0255 dataItem.id = fileInfo.fileName().toUInt(); 0256 dataItem.type = fileTypeFromPath(fileInfo.absoluteFilePath()); 0257 if (fileInfo.isFile()) { 0258 dataItem.filename = fileInfo.symLinkTarget(); 0259 } else { 0260 dataItem.filename = symLinkTargetFromPath(fileInfo.absoluteFilePath()); 0261 } 0262 data << std::move(dataItem); 0263 } 0264 } 0265 } 0266 0267 m_dataModel->setData(std::move(data)); 0268 } 0269 0270 void OpenFilesTab::onProxyModelChanged() 0271 { 0272 if (m_proxyModel->rowCount() > 0) { 0273 m_placeholderLabel->hide(); 0274 } else { 0275 if (m_proxyModel->sourceModel()->rowCount() == 0) { 0276 m_placeholderLabel->setText(i18nc("@info:status", "No data to display")); 0277 } else { 0278 m_placeholderLabel->setText(i18nc("@info:status", "No data matching the filter")); 0279 } 0280 m_placeholderLabel->show(); 0281 } 0282 } 0283 0284 void OpenFilesTab::onSearchEditEditingFinished() 0285 { 0286 m_proxyModel->setFilterFixedString(m_searchEdit->text()); 0287 }