/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2026 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include "LogView.h" #include #include #include #include #include #include #include "Code/QRDUtils.h" #include "Widgets/Extended/RDHeaderView.h" #include "ui_LogView.h" enum Columns { Column_Source, Column_PID, Column_Timestamp, Column_Location, Column_Type, Column_Message, Column_Count, }; class LogItemModel : public QAbstractItemModel { public: LogItemModel(LogView *view) : QAbstractItemModel(view), m_Viewer(view) {} void addRows(int numLines) { int count = rowCount(); emit beginInsertRows(QModelIndex(), count - numLines, count - 1); emit endInsertRows(); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if(row < 0 || row >= rowCount()) return QModelIndex(); return createIndex(row, column); } QModelIndex parent(const QModelIndex &index) const override { return QModelIndex(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if(parent == QModelIndex()) return m_Viewer->m_Messages.count(); return 0; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column_Count; } Qt::ItemFlags flags(const QModelIndex &index) const override { if(!index.isValid()) return 0; return QAbstractItemModel::flags(index); } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case Column_Source: return lit("Source"); case Column_PID: return lit("PID"); case Column_Timestamp: return lit("Timestamp"); case Column_Location: return lit("Location"); case Column_Type: return lit("Type"); case Column_Message: return lit("Message"); default: break; } } return QVariant(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if(index.isValid()) { int row = index.row(); int col = index.column(); if(col >= 0 && col < columnCount() && row < rowCount()) { const LogMessage &msg = m_Viewer->m_Messages[row]; if(role == Qt::DisplayRole) { switch(col) { case Column_Source: return msg.Source; case Column_PID: return QString::number(msg.PID); case Column_Timestamp: return msg.Timestamp.toString(lit("HH:mm:ss")); case Column_Location: return msg.Location; case Column_Type: return ToQStr(msg.Type); case Column_Message: { QVariant desc = msg.Message; RichResourceTextInitialise(desc, &m_Viewer->m_Ctx); return desc; } default: break; } } else if(msg.Type == LogType::Error) { if(role == Qt::BackgroundRole) return QBrush(QColor(255, 70, 70)); if(role == Qt::ForegroundRole) return QBrush(QColor(0, 0, 0)); } } } return QVariant(); } private: LogView *m_Viewer; }; class LogFilterModel : public QAbstractProxyModel { public: LogFilterModel(LogView *view) : QAbstractProxyModel(view), m_Viewer(view) {} bool m_UseRegexp = false; bool m_IncludeTextMatches = true; QString m_FilterText; QRegularExpression m_FilterRegexp; QSet m_HiddenPIDs; QSet m_HiddenTypes; void refresh() { emit beginResetModel(); m_VisibleRows.clear(); for(int i = 0; i < sourceModel()->rowCount(); i++) if(isVisibleRow(i)) m_VisibleRows.push_back(i); emit endResetModel(); } void addRows(int addedRows) { int numRows = sourceModel()->rowCount(); emit beginInsertRows(QModelIndex(), numRows - addedRows, numRows - 1); m_VisibleRows.reserve(m_VisibleRows.count() + addedRows); for(int i = 0; i < addedRows; i++) if(isVisibleRow(numRows - addedRows + i)) m_VisibleRows.push_back(numRows - addedRows + i); emit endInsertRows(); } virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override { auto it = std::lower_bound(m_VisibleRows.begin(), m_VisibleRows.end(), sourceIndex.row()); int row = -1; if(it != m_VisibleRows.end() && *it == sourceIndex.row()) row = it - m_VisibleRows.begin(); return createIndex(row, sourceIndex.column(), sourceIndex.internalId()); } virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const override { int row = -1; if(proxyIndex.row() >= 0 && proxyIndex.row() < m_VisibleRows.count()) row = m_VisibleRows[proxyIndex.row()]; return sourceModel()->index(row, proxyIndex.column()); } virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override { return m_VisibleRows.count(); } virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override { return sourceModel()->columnCount(parent); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { return createIndex(row, column); } QModelIndex parent(const QModelIndex &index) const override { return sourceModel()->parent(index); } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if(orientation == Qt::Horizontal) { return sourceModel()->headerData(section, orientation, role); } else if(section >= 0 && section < m_VisibleRows.count()) { return sourceModel()->headerData(m_VisibleRows[section], orientation, role); } return QVariant(); } void itemChanged(const QModelIndex &idx, const QVector &roles) { QModelIndex topLeft = index(idx.row(), 0); QModelIndex bottomRight = index(idx.row(), columnCount() - 1); emit dataChanged(topLeft, bottomRight, roles); } protected: QVector m_VisibleRows; bool isVisibleRow(int sourceRow) const { const LogMessage &msg = m_Viewer->m_Messages[sourceRow]; if(m_HiddenPIDs.contains(msg.PID)) return false; if(m_HiddenTypes.contains((uint32_t)msg.Type)) return false; if(m_UseRegexp) { if(m_FilterRegexp.isValid()) { return (m_FilterRegexp.match(msg.Message).hasMatch()) == m_IncludeTextMatches; } } else { if(!m_FilterText.isEmpty()) { return (msg.Message.contains(m_FilterText, Qt::CaseInsensitive)) == m_IncludeTextMatches; } } return true; } private: LogView *m_Viewer; }; static QList logTypeStrings; LogView::LogView(ICaptureContext &ctx, QWidget *parent) : QFrame(parent), ui(new Ui::LogView), m_Ctx(ctx) { ui->setupUi(this); m_ItemModel = new LogItemModel(this); m_FilterModel = new LogFilterModel(this); m_FilterModel->setSourceModel(m_ItemModel); ui->messages->setModel(m_FilterModel); ui->messages->viewport()->installEventFilter(this); m_delegate = new RichTextViewDelegate(ui->messages); ui->messages->setItemDelegate(m_delegate); ui->messages->setMouseTracking(true); ui->messages->setFont(Formatter::FixedFont()); m_TypeModel = new QStandardItemModel(0, 1, this); m_TypeModel->appendRow(new QStandardItem(tr("Log Type"))); for(LogType type : values()) { logTypeStrings.push_back(ToQStr(type)); QStandardItem *item = new QStandardItem(ToQStr(type)); item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); item->setData(Qt::Checked, Qt::CheckStateRole); m_TypeModel->appendRow(item); } ui->typeFilter->setModel(m_TypeModel); m_PIDModel = new QStandardItemModel(0, 1, this); m_PIDModel->appendRow(new QStandardItem(tr("PID"))); ui->pidFilter->setModel(m_PIDModel); ui->messages->header()->setSectionResizeMode(QHeaderView::Fixed); for(int c = 0; c < m_ItemModel->columnCount(); c++) ui->messages->resizeColumnToContents(c); messages_refresh(); QObject::connect(m_TypeModel, &QStandardItemModel::itemChanged, this, &LogView::typeFilter_changed); QObject::connect(m_PIDModel, &QStandardItemModel::itemChanged, this, &LogView::pidFilter_changed); QObject::connect(ui->messages, &RDTreeView::keyPress, this, &LogView::messages_keyPress); QObject::connect(&m_RefreshTimer, &QTimer::timeout, this, &LogView::messages_refresh); m_RefreshTimer.setSingleShot(false); m_RefreshTimer.setInterval(125); m_RefreshTimer.start(); } LogView::~LogView() { m_Ctx.BuiltinWindowClosed(this); m_Messages.clear(); delete ui; } void LogView::on_openExternal_clicked() { QString logPath = QString::fromUtf8(RENDERDOC_GetLogFile()); if(QFileInfo::exists(logPath)) QDesktopServices::openUrl(QUrl::fromLocalFile(logPath)); } void LogView::on_save_clicked() { QString filename = RDDialog::getSaveFileName(this, tr("Export log to disk"), QString(), tr("Log Files (*.log);;Text files (*.txt);;All files (*)")); if(filename.isEmpty()) return; QFile *f = new QFile(filename); if(!f->open(QIODevice::WriteOnly | QFile::Truncate)) { delete f; RDDialog::critical(this, tr("Error exporting log"), tr("Couldn't open file '%1' for writing").arg(filename)); return; } rdcstr contents; RENDERDOC_GetLogFileContents(0, contents); f->write(QByteArray(contents.c_str(), contents.count())); delete f; } void LogView::on_textFilter_textChanged(const QString &text) { m_FilterModel->m_FilterText = text; m_FilterModel->m_FilterRegexp = QRegularExpression(text); m_FilterModel->m_FilterRegexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); m_FilterModel->refresh(); } void LogView::on_textFilterMeaning_currentIndexChanged(int index) { // 0 is Include, 1 is Exclude m_FilterModel->m_IncludeTextMatches = (index == 0); m_FilterModel->refresh(); } void LogView::on_regexpFilter_toggled() { m_FilterModel->m_UseRegexp = ui->regexpFilter->isChecked(); m_FilterModel->refresh(); } void LogView::messages_keyPress(QKeyEvent *event) { if(event->matches(QKeySequence::Copy)) { QModelIndexList items = ui->messages->selectionModel()->selectedIndexes(); QList rows; for(QModelIndex idx : items) { if(!rows.contains(idx.row())) rows.push_back(idx.row()); } std::sort(rows.begin(), rows.end()); int columns = m_ItemModel->columnCount(); QString clipboardText; for(int r : rows) { const LogMessage &msg = m_Messages[r]; clipboardText += QFormatStr("%1 PID %2: [%3] %4 - %5 - %6\n") .arg(msg.Source, -8) .arg(msg.PID, 6) .arg(msg.Timestamp.toString(lit("HH:mm:ss"))) .arg(msg.Location, 26) .arg(ToQStr(msg.Type), -7) .arg(msg.Message); } QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(clipboardText.trimmed()); } } void LogView::typeFilter_changed(QStandardItem *item) { uint32_t type = m_TypeModel->indexFromItem(item).row() - 1; if(item->checkState() == Qt::Checked) m_FilterModel->m_HiddenTypes.remove(type); else m_FilterModel->m_HiddenTypes.insert(type); m_FilterModel->refresh(); ui->typeFilter->setCurrentIndex(0); } bool LogView::eventFilter(QObject *watched, QEvent *event) { if(watched == ui->messages->viewport() && event->type() == QEvent::MouseMove) { bool ret = QObject::eventFilter(watched, event); if(m_delegate->linkHover((QMouseEvent *)event, font(), ui->messages->currentHoverIndex())) { m_FilterModel->itemChanged(ui->messages->currentHoverIndex(), {Qt::DecorationRole}); ui->messages->setCursor(QCursor(Qt::PointingHandCursor)); } else { ui->messages->unsetCursor(); } return ret; } return QObject::eventFilter(watched, event); } void LogView::pidFilter_changed(QStandardItem *item) { uint32_t PID = item->text().toUInt(); if(item->checkState() == Qt::Checked) m_FilterModel->m_HiddenPIDs.remove(PID); else m_FilterModel->m_HiddenPIDs.insert(PID); m_FilterModel->refresh(); ui->pidFilter->setCurrentIndex(0); } void LogView::messages_refresh() { rdcstr contents; RENDERDOC_GetLogFileContents(prevOffset, contents); if(contents.empty()) return; // look at all new lines since the last one we saw QStringList lines = QString(contents).split(QRegularExpression(lit("[\r\n]"))); prevOffset += contents.size(); QString r = lit("^" // start of the line "([A-Z][A-Z][A-Z][A-Z]) " // project "([0-9]+): " // PID "\\[([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\\] " // timestamp "\\s*([^(]+)\\(\\s*([0-9]+)\\) - " // filename.ext( line) "([A-Za-z]+)\\s+- " // type "(.*)"); QRegularExpression logRegex(r); int prevCount = m_Messages.count(); for(const QString &line : lines) { QRegularExpressionMatch match = logRegex.match(line); if(match.hasMatch()) { LogMessage msg; msg.Source = match.captured(1); if(msg.Source == lit("ADRD")) msg.Source = tr("Android"); else if(msg.Source == lit("QTRD")) msg.Source = tr("UI"); else if(msg.Source == lit("RDOC")) msg.Source = tr("Core"); msg.PID = match.captured(2).toUInt(); msg.Timestamp = QTime(match.captured(3).toUInt(), match.captured(4).toUInt(), match.captured(5).toUInt()); msg.Location = QFormatStr("%1(%2)").arg(match.captured(6)).arg(match.captured(7)); msg.Type = (LogType)logTypeStrings.indexOf(match.captured(8)); msg.Message = match.captured(9).trimmed(); m_Messages.push_back(msg); if(!m_PIDs.contains(msg.PID)) { m_PIDs.append(msg.PID); QStandardItem *item = new QStandardItem(QString::number(msg.PID)); item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); item->setData(Qt::Checked, Qt::CheckStateRole); m_PIDModel->appendRow(item); } } } if(!lines.isEmpty()) { m_ItemModel->addRows(m_Messages.count() - prevCount); m_FilterModel->addRows(m_Messages.count() - prevCount); } // go through each new message and size up columns to fit for(int i = prevCount; i < m_Messages.count(); i++) { for(int c = 0; c < ui->messages->model()->columnCount(); c++) { QSize s = ui->messages->sizeHintForIndex(ui->messages->model()->index(i, c)); int w = ui->messages->header()->sectionSize(c); if(s.width() > w) ui->messages->header()->resizeSection(c, s.width()); } } if(ui->followNew->isChecked()) ui->messages->scrollToBottom(); }