2022-08-19 15:59:36 +02:00
|
|
|
// Copyright (C) 2021 The Qt Company Ltd.
|
2022-12-21 10:12:09 +01:00
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
2021-08-19 22:32:21 +02:00
|
|
|
|
|
|
|
|
#include "loggingviewer.h"
|
|
|
|
|
|
|
|
|
|
#include "coreicons.h"
|
2023-01-16 17:20:07 +01:00
|
|
|
#include "coreplugintr.h"
|
2021-08-19 22:32:21 +02:00
|
|
|
#include "icore.h"
|
|
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
#include <utils/async.h>
|
2021-08-19 22:32:21 +02:00
|
|
|
#include <utils/basetreeview.h>
|
2023-10-16 13:15:17 +02:00
|
|
|
#include <utils/fancylineedit.h>
|
2022-07-22 13:07:53 +02:00
|
|
|
#include <utils/fileutils.h>
|
2023-10-13 12:28:52 +02:00
|
|
|
#include <utils/layoutbuilder.h>
|
2021-08-19 22:32:21 +02:00
|
|
|
#include <utils/listmodel.h>
|
|
|
|
|
#include <utils/qtcassert.h>
|
2023-10-13 12:28:52 +02:00
|
|
|
#include <utils/stringutils.h>
|
2021-08-19 22:32:21 +02:00
|
|
|
#include <utils/theme/theme.h>
|
|
|
|
|
#include <utils/utilsicons.h>
|
|
|
|
|
|
|
|
|
|
#include <QAction>
|
|
|
|
|
#include <QColorDialog>
|
|
|
|
|
#include <QDialog>
|
|
|
|
|
#include <QHBoxLayout>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
|
#include <QMenu>
|
|
|
|
|
#include <QMessageBox>
|
|
|
|
|
#include <QPushButton>
|
|
|
|
|
#include <QSortFilterProxyModel>
|
2023-10-13 12:28:52 +02:00
|
|
|
#include <QSplitter>
|
2021-08-19 22:32:21 +02:00
|
|
|
#include <QToolButton>
|
|
|
|
|
#include <QVBoxLayout>
|
2023-10-13 12:28:52 +02:00
|
|
|
namespace Core::Internal {
|
2021-08-19 22:32:21 +02:00
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
static QColor colorForCategory(const QString &category);
|
|
|
|
|
void setCategoryColor(const QString &category, const QColor &color);
|
|
|
|
|
|
|
|
|
|
QHash<QString, QColor> s_categoryColor;
|
|
|
|
|
|
|
|
|
|
static inline QString messageTypeToString(QtMsgType type)
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
switch (type) {
|
|
|
|
|
case QtDebugMsg:
|
|
|
|
|
return {"Debug"};
|
|
|
|
|
case QtInfoMsg:
|
|
|
|
|
return {"Info"};
|
|
|
|
|
case QtCriticalMsg:
|
|
|
|
|
return {"Critical"};
|
|
|
|
|
case QtWarningMsg:
|
|
|
|
|
return {"Warning"};
|
|
|
|
|
case QtFatalMsg:
|
|
|
|
|
return {"Fatal"};
|
|
|
|
|
default:
|
|
|
|
|
return {"Unknown"};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class LogCategoryRegistry : public QObject
|
|
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
2021-08-19 22:32:21 +02:00
|
|
|
public:
|
2023-10-13 12:28:52 +02:00
|
|
|
static LogCategoryRegistry &instance()
|
|
|
|
|
{
|
|
|
|
|
static LogCategoryRegistry s_instance;
|
|
|
|
|
return s_instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void filter(QLoggingCategory *category)
|
|
|
|
|
{
|
|
|
|
|
if (s_oldFilter)
|
|
|
|
|
s_oldFilter(category);
|
|
|
|
|
|
|
|
|
|
LogCategoryRegistry::instance().onFilter(category);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void start()
|
|
|
|
|
{
|
|
|
|
|
if (m_started)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_started = true;
|
|
|
|
|
s_oldFilter = QLoggingCategory::installFilter(&LogCategoryRegistry::filter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<QLoggingCategory *> categories() { return m_categories; }
|
|
|
|
|
|
|
|
|
|
signals:
|
|
|
|
|
void newLogCategory(QLoggingCategory *category);
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
LogCategoryRegistry() = default;
|
|
|
|
|
|
|
|
|
|
void onFilter(QLoggingCategory *category)
|
|
|
|
|
{
|
2023-10-30 08:25:29 +01:00
|
|
|
if (QThread::currentThread() != thread()) {
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this, [category, this] { onFilter(category); }, Qt::QueuedConnection);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
if (!m_categories.contains(category)) {
|
|
|
|
|
m_categories.append(category);
|
|
|
|
|
emit newLogCategory(category);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
static QLoggingCategory::CategoryFilter s_oldFilter;
|
|
|
|
|
|
|
|
|
|
QList<QLoggingCategory *> m_categories;
|
|
|
|
|
bool m_started{false};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QLoggingCategory::CategoryFilter LogCategoryRegistry::s_oldFilter;
|
|
|
|
|
|
|
|
|
|
struct SavedEntry
|
|
|
|
|
{
|
|
|
|
|
QColor color;
|
2021-08-19 22:32:21 +02:00
|
|
|
QString name;
|
2023-10-13 12:28:52 +02:00
|
|
|
QtMsgType level;
|
|
|
|
|
std::optional<std::array<bool, 5>> levels;
|
|
|
|
|
|
|
|
|
|
static Utils::expected_str<SavedEntry> fromJson(const QJsonObject &obj)
|
|
|
|
|
{
|
|
|
|
|
if (!obj.contains("name"))
|
|
|
|
|
return Utils::make_unexpected(Tr::tr("Entry is missing a logging category name."));
|
2021-08-19 22:32:21 +02:00
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
SavedEntry result;
|
|
|
|
|
result.name = obj.value("name").toString();
|
|
|
|
|
|
|
|
|
|
if (!obj.contains("entry"))
|
|
|
|
|
return Utils::make_unexpected(Tr::tr("Entry is missing data."));
|
|
|
|
|
|
|
|
|
|
auto entry = obj.value("entry").toObject();
|
|
|
|
|
if (entry.contains("color"))
|
|
|
|
|
result.color = QColor(entry.value("color").toString());
|
|
|
|
|
|
|
|
|
|
if (entry.contains("level")) {
|
|
|
|
|
int lvl = entry.value("level").toInt(0);
|
|
|
|
|
if (lvl < QtDebugMsg || lvl > QtInfoMsg)
|
|
|
|
|
return Utils::make_unexpected(Tr::tr("Invalid level: %1").arg(lvl));
|
|
|
|
|
result.level = static_cast<QtMsgType>(lvl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry.contains("levels")) {
|
|
|
|
|
QVariantMap map = entry.value("levels").toVariant().toMap();
|
|
|
|
|
std::array<bool, 5> levels{
|
|
|
|
|
map.contains("Debug") && map["Debug"].toBool(),
|
|
|
|
|
map.contains("Warning") && map["Warning"].toBool(),
|
|
|
|
|
map.contains("Critical") && map["Critical"].toBool(),
|
|
|
|
|
true,
|
|
|
|
|
map.contains("Info") && map["Info"].toBool(),
|
|
|
|
|
};
|
|
|
|
|
result.levels = levels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
};
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
class LoggingCategoryEntry
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
public:
|
|
|
|
|
LoggingCategoryEntry(const QString &name)
|
|
|
|
|
: m_name(name)
|
|
|
|
|
{}
|
|
|
|
|
LoggingCategoryEntry(QLoggingCategory *category)
|
|
|
|
|
: m_name(QString::fromUtf8(category->categoryName()))
|
|
|
|
|
{
|
|
|
|
|
setLogCategory(category);
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
LoggingCategoryEntry(const SavedEntry &savedEntry)
|
|
|
|
|
: m_name(savedEntry.name)
|
|
|
|
|
{
|
|
|
|
|
m_saved = savedEntry.levels;
|
|
|
|
|
m_color = savedEntry.color;
|
|
|
|
|
if (!m_saved) {
|
|
|
|
|
m_saved = std::array<bool, 5>();
|
|
|
|
|
for (int i = QtDebugMsg; i <= QtInfoMsg; ++i) {
|
|
|
|
|
(*m_saved)[i] = savedEntry.level <= i;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
QString name() const { return m_name; }
|
|
|
|
|
QColor color() const { return m_color; }
|
|
|
|
|
void setColor(const QColor &c) { m_color = c; }
|
|
|
|
|
|
|
|
|
|
void setUseOriginal(bool useOriginal)
|
|
|
|
|
{
|
|
|
|
|
if (!m_useOriginal && m_category && m_originalSettings) {
|
|
|
|
|
m_saved = std::array<bool, 5>{};
|
|
|
|
|
|
|
|
|
|
for (int i = QtDebugMsg; i < QtInfoMsg; i++) {
|
|
|
|
|
(*m_saved)[i] = m_category->isEnabled(static_cast<QtMsgType>(i));
|
|
|
|
|
m_category->setEnabled(static_cast<QtMsgType>(i), (*m_originalSettings)[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (!useOriginal && m_useOriginal && m_saved && m_category) {
|
|
|
|
|
for (int i = QtDebugMsg; i < QtInfoMsg; i++)
|
|
|
|
|
m_category->setEnabled(static_cast<QtMsgType>(i), (*m_saved)[i]);
|
|
|
|
|
}
|
|
|
|
|
m_useOriginal = useOriginal;
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
bool isEnabled(QtMsgType msgType) const
|
|
|
|
|
{
|
|
|
|
|
if (m_category)
|
|
|
|
|
return m_category->isEnabled(msgType);
|
|
|
|
|
if (m_saved)
|
|
|
|
|
return (*m_saved)[msgType];
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
bool isEnabledOriginally(QtMsgType msgType) const
|
|
|
|
|
{
|
|
|
|
|
if (m_originalSettings)
|
|
|
|
|
return (*m_originalSettings)[msgType];
|
|
|
|
|
return isEnabled(msgType);
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void setEnabled(QtMsgType msgType, bool isEnabled)
|
|
|
|
|
{
|
|
|
|
|
QTC_ASSERT(!m_useOriginal, return);
|
|
|
|
|
|
|
|
|
|
if (m_category)
|
|
|
|
|
m_category->setEnabled(msgType, isEnabled);
|
|
|
|
|
|
|
|
|
|
if (m_saved)
|
|
|
|
|
(*m_saved)[msgType] = isEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setSaved(const SavedEntry &entry)
|
|
|
|
|
{
|
|
|
|
|
QTC_ASSERT(entry.name == name(), return);
|
|
|
|
|
|
|
|
|
|
m_saved = entry.levels;
|
|
|
|
|
m_color = entry.color;
|
|
|
|
|
if (!m_saved) {
|
|
|
|
|
m_saved = std::array<bool, 5>();
|
|
|
|
|
for (int i = QtDebugMsg; i <= QtInfoMsg; ++i) {
|
|
|
|
|
(*m_saved)[i] = entry.level <= i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (m_category)
|
|
|
|
|
setLogCategory(m_category);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setLogCategory(QLoggingCategory *category)
|
|
|
|
|
{
|
|
|
|
|
QTC_ASSERT(QString::fromUtf8(category->categoryName()) == m_name, return);
|
|
|
|
|
|
|
|
|
|
m_category = category;
|
|
|
|
|
if (!m_originalSettings) {
|
|
|
|
|
m_originalSettings = {
|
|
|
|
|
category->isDebugEnabled(),
|
|
|
|
|
category->isWarningEnabled(),
|
|
|
|
|
category->isCriticalEnabled(),
|
|
|
|
|
true, // always enable fatal
|
|
|
|
|
category->isInfoEnabled(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_saved && !m_useOriginal) {
|
|
|
|
|
m_category->setEnabled(QtDebugMsg, m_saved->at(0));
|
|
|
|
|
m_category->setEnabled(QtWarningMsg, m_saved->at(1));
|
|
|
|
|
m_category->setEnabled(QtCriticalMsg, m_saved->at(2));
|
|
|
|
|
m_category->setEnabled(QtInfoMsg, m_saved->at(4));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isDebugEnabled() const { return isEnabled(QtDebugMsg); }
|
|
|
|
|
bool isWarningEnabled() const { return isEnabled(QtWarningMsg); }
|
|
|
|
|
bool isCriticalEnabled() const { return isEnabled(QtCriticalMsg); }
|
|
|
|
|
bool isInfoEnabled() const { return isEnabled(QtInfoMsg); }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
QString m_name;
|
|
|
|
|
QLoggingCategory *m_category{nullptr};
|
|
|
|
|
std::optional<std::array<bool, 5>> m_originalSettings;
|
|
|
|
|
std::optional<std::array<bool, 5>> m_saved;
|
|
|
|
|
QColor m_color;
|
|
|
|
|
bool m_useOriginal{false};
|
|
|
|
|
};
|
2021-08-19 22:32:21 +02:00
|
|
|
|
|
|
|
|
class LoggingCategoryModel : public QAbstractListModel
|
|
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
public:
|
2023-10-13 12:28:52 +02:00
|
|
|
LoggingCategoryModel(QObject *parent)
|
|
|
|
|
: QAbstractListModel(parent)
|
|
|
|
|
{
|
|
|
|
|
auto newCategory = [this](QLoggingCategory *category) {
|
|
|
|
|
QString name = QString::fromUtf8(category->categoryName());
|
|
|
|
|
auto itExists = std::find_if(m_categories.begin(),
|
|
|
|
|
m_categories.end(),
|
|
|
|
|
[name](const auto &cat) { return name == cat.name(); });
|
|
|
|
|
|
|
|
|
|
if (itExists != m_categories.end()) {
|
|
|
|
|
itExists->setLogCategory(category);
|
|
|
|
|
} else {
|
|
|
|
|
LoggingCategoryEntry entry(category);
|
|
|
|
|
append(entry);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (QLoggingCategory *cat : LogCategoryRegistry::instance().categories())
|
|
|
|
|
newCategory(cat);
|
|
|
|
|
|
|
|
|
|
connect(&LogCategoryRegistry::instance(),
|
|
|
|
|
&LogCategoryRegistry::newLogCategory,
|
|
|
|
|
this,
|
|
|
|
|
newCategory);
|
|
|
|
|
|
|
|
|
|
LogCategoryRegistry::instance().start();
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
~LoggingCategoryModel() override;
|
2023-10-13 12:28:52 +02:00
|
|
|
enum Column { Color, Name, Debug, Warning, Critical, Fatal, Info };
|
2021-08-19 22:32:21 +02:00
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
enum Role { OriginalStateRole = Qt::UserRole + 1 };
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void append(const LoggingCategoryEntry &entry);
|
|
|
|
|
int columnCount(const QModelIndex &) const final { return 7; }
|
|
|
|
|
int rowCount(const QModelIndex & = QModelIndex()) const final { return m_categories.size(); }
|
2021-08-19 22:32:21 +02:00
|
|
|
QVariant data(const QModelIndex &index, int role) const final;
|
|
|
|
|
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) final;
|
|
|
|
|
Qt::ItemFlags flags(const QModelIndex &index) const final;
|
2023-10-13 12:28:52 +02:00
|
|
|
QVariant headerData(int section,
|
|
|
|
|
Qt::Orientation orientation,
|
2021-08-19 22:32:21 +02:00
|
|
|
int role = Qt::DisplayRole) const final;
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void saveEnabledCategoryPreset() const;
|
|
|
|
|
void loadAndUpdateFromPreset();
|
|
|
|
|
|
|
|
|
|
void setUseOriginal(bool useOriginal)
|
|
|
|
|
{
|
|
|
|
|
if (useOriginal != m_useOriginal) {
|
|
|
|
|
beginResetModel();
|
|
|
|
|
for (auto &entry : m_categories)
|
|
|
|
|
entry.setUseOriginal(useOriginal);
|
|
|
|
|
|
|
|
|
|
m_useOriginal = useOriginal;
|
|
|
|
|
endResetModel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QList<LoggingCategoryEntry> &categories() const { return m_categories; }
|
2021-08-19 22:32:21 +02:00
|
|
|
|
|
|
|
|
private:
|
2023-10-13 12:28:52 +02:00
|
|
|
QList<LoggingCategoryEntry> m_categories;
|
|
|
|
|
bool m_useOriginal{false};
|
2021-08-19 22:32:21 +02:00
|
|
|
};
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
LoggingCategoryModel::~LoggingCategoryModel() {}
|
2021-08-19 22:32:21 +02:00
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void LoggingCategoryModel::append(const LoggingCategoryEntry &entry)
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
beginInsertRows(QModelIndex(), m_categories.size(), m_categories.size() + 1);
|
|
|
|
|
m_categories.push_back(entry);
|
2021-08-19 22:32:21 +02:00
|
|
|
endInsertRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant LoggingCategoryModel::data(const QModelIndex &index, int role) const
|
|
|
|
|
{
|
|
|
|
|
if (!index.isValid())
|
|
|
|
|
return {};
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
if (index.column() == Column::Name && role == Qt::DisplayRole) {
|
|
|
|
|
return m_categories.at(index.row()).name();
|
|
|
|
|
} else if (role == Qt::DecorationRole && index.column() == Column::Color) {
|
|
|
|
|
const QColor color = m_categories.at(index.row()).color();
|
2021-08-19 22:32:21 +02:00
|
|
|
if (color.isValid())
|
|
|
|
|
return color;
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
static const QColor defaultColor = Utils::creatorTheme()->palette().text().color();
|
2021-08-19 22:32:21 +02:00
|
|
|
return defaultColor;
|
2023-10-16 13:15:17 +02:00
|
|
|
} else if (index.column() >= Column::Debug && index.column() <= Column::Info) {
|
|
|
|
|
if (role == Qt::CheckStateRole) {
|
|
|
|
|
const LoggingCategoryEntry &entry = m_categories.at(index.row());
|
|
|
|
|
const bool isEnabled = entry.isEnabled(
|
|
|
|
|
static_cast<QtMsgType>(index.column() - Column::Debug));
|
|
|
|
|
return isEnabled ? Qt::Checked : Qt::Unchecked;
|
|
|
|
|
} else if (role == OriginalStateRole) {
|
|
|
|
|
const LoggingCategoryEntry &entry = m_categories.at(index.row());
|
|
|
|
|
return entry.isEnabledOriginally(static_cast<QtMsgType>(index.column() - Column::Debug))
|
|
|
|
|
? Qt::Checked
|
|
|
|
|
: Qt::Unchecked;
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool LoggingCategoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
|
|
|
{
|
|
|
|
|
if (!index.isValid())
|
|
|
|
|
return false;
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
if (role == Qt::CheckStateRole && index.column() >= Column::Debug
|
|
|
|
|
&& index.column() <= Column::Info) {
|
|
|
|
|
QtMsgType msgType = static_cast<QtMsgType>(index.column() - Column::Debug);
|
|
|
|
|
auto &entry = m_categories[index.row()];
|
|
|
|
|
bool isEnabled = entry.isEnabled(msgType);
|
|
|
|
|
|
|
|
|
|
const Qt::CheckState current = isEnabled ? Qt::Checked : Qt::Unchecked;
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
if (current != value.toInt()) {
|
2023-10-13 12:28:52 +02:00
|
|
|
entry.setEnabled(msgType, value.toInt() == Qt::Checked);
|
2021-08-19 22:32:21 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
} else if (role == Qt::DecorationRole && index.column() == Column::Color) {
|
|
|
|
|
auto &category = m_categories[index.row()];
|
|
|
|
|
QColor currentColor = category.color();
|
2021-08-19 22:32:21 +02:00
|
|
|
QColor color = value.value<QColor>();
|
2023-10-13 12:28:52 +02:00
|
|
|
if (color.isValid() && color != currentColor) {
|
|
|
|
|
category.setColor(color);
|
|
|
|
|
setCategoryColor(category.name(), color);
|
|
|
|
|
emit dataChanged(index, index, {Qt::DisplayRole});
|
2021-08-19 22:32:21 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Qt::ItemFlags LoggingCategoryModel::flags(const QModelIndex &index) const
|
|
|
|
|
{
|
|
|
|
|
if (!index.isValid())
|
|
|
|
|
return Qt::NoItemFlags;
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
if (index.column() == LoggingCategoryModel::Column::Fatal)
|
|
|
|
|
return Qt::NoItemFlags;
|
|
|
|
|
|
|
|
|
|
if (index.column() == Column::Name || index.column() == Column::Color)
|
|
|
|
|
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
|
|
|
|
|
|
|
|
|
if (m_useOriginal)
|
|
|
|
|
return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
|
|
|
|
|
|
|
|
|
|
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant LoggingCategoryModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
if (role == Qt::DisplayRole && orientation == Qt::Horizontal && section >= 0 && section < 8) {
|
2021-08-19 22:32:21 +02:00
|
|
|
switch (section) {
|
2023-10-13 12:28:52 +02:00
|
|
|
case Column::Name:
|
|
|
|
|
return Tr::tr("Category");
|
|
|
|
|
case Column::Color:
|
|
|
|
|
return Tr::tr("Color");
|
|
|
|
|
case Column::Debug:
|
|
|
|
|
return Tr::tr("Debug");
|
|
|
|
|
case Column::Warning:
|
|
|
|
|
return Tr::tr("Warning");
|
|
|
|
|
case Column::Critical:
|
|
|
|
|
return Tr::tr("Critical");
|
|
|
|
|
case Column::Fatal:
|
|
|
|
|
return Tr::tr("Fatal");
|
|
|
|
|
case Column::Info:
|
|
|
|
|
return Tr::tr("Info");
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class LogEntry
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
QString timestamp;
|
|
|
|
|
QString type;
|
2023-10-13 12:28:52 +02:00
|
|
|
QString category;
|
2021-08-19 22:32:21 +02:00
|
|
|
QString message;
|
|
|
|
|
|
|
|
|
|
QString outputLine(bool printTimestamp, bool printType) const
|
|
|
|
|
{
|
|
|
|
|
QString line;
|
|
|
|
|
if (printTimestamp)
|
|
|
|
|
line.append(timestamp + ' ');
|
|
|
|
|
line.append(category);
|
|
|
|
|
if (printType)
|
|
|
|
|
line.append('.' + type.toLower());
|
|
|
|
|
line.append(": ");
|
|
|
|
|
line.append(message);
|
|
|
|
|
line.append('\n');
|
|
|
|
|
return line;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
class LoggingEntryModel : public Utils::ListModel<LogEntry>
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
~LoggingEntryModel() { qInstallMessageHandler(m_originalMessageHandler); }
|
|
|
|
|
|
|
|
|
|
static void logMessageHandler(QtMsgType type,
|
|
|
|
|
const QMessageLogContext &context,
|
|
|
|
|
const QString &mssg)
|
|
|
|
|
{
|
|
|
|
|
instance().msgHandler(type, context, mssg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QVariant logEntryDataAccessor(const LogEntry &entry, int column, int role)
|
|
|
|
|
{
|
|
|
|
|
if (column >= 0 && column <= 3 && (role == Qt::DisplayRole || role == Qt::ToolTipRole)) {
|
|
|
|
|
switch (column) {
|
|
|
|
|
case 0:
|
|
|
|
|
return entry.timestamp;
|
|
|
|
|
case 1:
|
|
|
|
|
return entry.category;
|
|
|
|
|
case 2:
|
|
|
|
|
return entry.type;
|
|
|
|
|
case 3: {
|
|
|
|
|
if (role == Qt::ToolTipRole)
|
|
|
|
|
return entry.message;
|
|
|
|
|
return entry.message.left(1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (role == Qt::TextAlignmentRole)
|
|
|
|
|
return Qt::AlignTop;
|
|
|
|
|
if (column == 1 && role == Qt::ForegroundRole)
|
|
|
|
|
return colorForCategory(entry.category);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static LoggingEntryModel &instance()
|
|
|
|
|
{
|
|
|
|
|
static LoggingEntryModel model;
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setEnabled(bool enabled) { m_enabled = enabled; }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
LoggingEntryModel()
|
|
|
|
|
{
|
|
|
|
|
setHeader({Tr::tr("Timestamp"), Tr::tr("Category"), Tr::tr("Type"), Tr::tr("Message")});
|
|
|
|
|
setDataAccessor(&logEntryDataAccessor);
|
|
|
|
|
|
|
|
|
|
m_originalMessageHandler = qInstallMessageHandler(logMessageHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
|
|
|
|
{
|
|
|
|
|
if (!m_enabled) {
|
|
|
|
|
m_originalMessageHandler(type, context, msg);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!context.category) {
|
|
|
|
|
m_originalMessageHandler(type, context, msg);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString category = QString::fromLocal8Bit(context.category);
|
|
|
|
|
|
|
|
|
|
const QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
|
|
|
|
|
|
|
|
|
|
if (rowCount() >= 1000000) // limit log to 1000000 items
|
|
|
|
|
destroyItem(itemForIndex(index(0, 0)));
|
|
|
|
|
|
|
|
|
|
appendItem(LogEntry{timestamp, messageTypeToString(type), category, msg});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
QtMessageHandler m_originalMessageHandler{nullptr};
|
|
|
|
|
bool m_enabled{true};
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
class LoggingViewManagerWidget : public QDialog
|
|
|
|
|
{
|
|
|
|
|
public:
|
2023-10-13 12:28:52 +02:00
|
|
|
~LoggingViewManagerWidget() { LoggingEntryModel::instance().setEnabled(false); }
|
|
|
|
|
|
|
|
|
|
static LoggingViewManagerWidget *instance()
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
static QPointer<LoggingViewManagerWidget> instance = new LoggingViewManagerWidget(
|
|
|
|
|
Core::ICore::dialogParent());
|
|
|
|
|
return instance;
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
protected:
|
|
|
|
|
void showEvent(QShowEvent *) override
|
|
|
|
|
{
|
|
|
|
|
if (!m_stopLog->isChecked())
|
|
|
|
|
m_categoryModel->setUseOriginal(false);
|
|
|
|
|
|
|
|
|
|
LoggingEntryModel::instance().setEnabled(!m_stopLog->isChecked());
|
|
|
|
|
}
|
|
|
|
|
void hideEvent(QHideEvent *) override
|
|
|
|
|
{
|
|
|
|
|
m_categoryModel->setUseOriginal(true);
|
|
|
|
|
LoggingEntryModel::instance().setEnabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
explicit LoggingViewManagerWidget(QWidget *parent);
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
private:
|
|
|
|
|
void showLogViewContextMenu(const QPoint &pos) const;
|
|
|
|
|
void showLogCategoryContextMenu(const QPoint &pos) const;
|
|
|
|
|
void saveLoggingsToFile() const;
|
2023-10-13 12:28:52 +02:00
|
|
|
QSortFilterProxyModel *m_sortFilterModel = nullptr;
|
2021-08-19 22:32:21 +02:00
|
|
|
LoggingCategoryModel *m_categoryModel = nullptr;
|
|
|
|
|
Utils::BaseTreeView *m_logView = nullptr;
|
|
|
|
|
Utils::BaseTreeView *m_categoryView = nullptr;
|
|
|
|
|
QToolButton *m_timestamps = nullptr;
|
|
|
|
|
QToolButton *m_messageTypes = nullptr;
|
2023-10-13 12:28:52 +02:00
|
|
|
QToolButton *m_stopLog = nullptr;
|
2021-08-19 22:32:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
LoggingViewManagerWidget::LoggingViewManagerWidget(QWidget *parent)
|
|
|
|
|
: QDialog(parent)
|
|
|
|
|
{
|
2023-01-16 17:20:07 +01:00
|
|
|
setWindowTitle(Tr::tr("Logging Category Viewer"));
|
2021-08-19 22:32:21 +02:00
|
|
|
auto save = new QToolButton;
|
|
|
|
|
save->setIcon(Utils::Icons::SAVEFILE.icon());
|
2023-01-16 17:20:07 +01:00
|
|
|
save->setToolTip(Tr::tr("Save Log"));
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
auto clean = new QToolButton;
|
|
|
|
|
clean->setIcon(Utils::Icons::CLEAN.icon());
|
2023-01-16 17:20:07 +01:00
|
|
|
clean->setToolTip(Tr::tr("Clear"));
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
m_stopLog = new QToolButton;
|
|
|
|
|
m_stopLog->setIcon(Utils::Icons::STOP_SMALL.icon());
|
|
|
|
|
m_stopLog->setToolTip(Tr::tr("Stop Logging"));
|
|
|
|
|
m_stopLog->setCheckable(true);
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
auto qtInternal = new QToolButton;
|
|
|
|
|
qtInternal->setIcon(Core::Icons::QTLOGO.icon());
|
2023-10-16 13:15:17 +02:00
|
|
|
qtInternal->setToolTip(Tr::tr("Filter Qt Internal Log Categories"));
|
|
|
|
|
qtInternal->setCheckable(false);
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
auto autoScroll = new QToolButton;
|
|
|
|
|
autoScroll->setIcon(Utils::Icons::ARROW_DOWN.icon());
|
2023-01-16 17:20:07 +01:00
|
|
|
autoScroll->setToolTip(Tr::tr("Auto Scroll"));
|
2021-08-19 22:32:21 +02:00
|
|
|
autoScroll->setCheckable(true);
|
|
|
|
|
autoScroll->setChecked(true);
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
m_timestamps = new QToolButton;
|
|
|
|
|
auto icon = Utils::Icon({{":/utils/images/stopwatch.png", Utils::Theme::PanelTextColorMid}},
|
|
|
|
|
Utils::Icon::Tint);
|
|
|
|
|
m_timestamps->setIcon(icon.icon());
|
2023-01-16 17:20:07 +01:00
|
|
|
m_timestamps->setToolTip(Tr::tr("Timestamps"));
|
2021-08-19 22:32:21 +02:00
|
|
|
m_timestamps->setCheckable(true);
|
|
|
|
|
m_timestamps->setChecked(true);
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
m_messageTypes = new QToolButton;
|
|
|
|
|
icon = Utils::Icon({{":/utils/images/message.png", Utils::Theme::PanelTextColorMid}},
|
|
|
|
|
Utils::Icon::Tint);
|
|
|
|
|
m_messageTypes->setIcon(icon.icon());
|
2023-01-16 17:20:07 +01:00
|
|
|
m_messageTypes->setToolTip(Tr::tr("Message Types"));
|
2021-08-19 22:32:21 +02:00
|
|
|
m_messageTypes->setCheckable(true);
|
|
|
|
|
m_messageTypes->setChecked(false);
|
|
|
|
|
|
|
|
|
|
m_logView = new Utils::BaseTreeView;
|
2023-10-13 12:28:52 +02:00
|
|
|
m_logView->setModel(&LoggingEntryModel::instance());
|
2022-01-17 08:07:08 +01:00
|
|
|
m_logView->setUniformRowHeights(true);
|
2021-08-19 22:32:21 +02:00
|
|
|
m_logView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
|
|
|
m_logView->setFrameStyle(QFrame::Box);
|
|
|
|
|
m_logView->setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
|
|
|
m_logView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
|
|
|
|
m_logView->setColumnHidden(2, true);
|
|
|
|
|
m_logView->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
m_categoryModel = new LoggingCategoryModel(this);
|
|
|
|
|
m_sortFilterModel = new QSortFilterProxyModel(m_categoryModel);
|
|
|
|
|
m_sortFilterModel->setSourceModel(m_categoryModel);
|
|
|
|
|
m_sortFilterModel->sort(LoggingCategoryModel::Column::Name);
|
|
|
|
|
m_sortFilterModel->setSortRole(Qt::DisplayRole);
|
|
|
|
|
m_sortFilterModel->setFilterKeyColumn(LoggingCategoryModel::Column::Name);
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
m_categoryView = new Utils::BaseTreeView;
|
|
|
|
|
m_categoryView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
|
|
|
m_categoryView->setFrameStyle(QFrame::Box);
|
|
|
|
|
m_categoryView->setAttribute(Qt::WA_MacShowFocusRect, false);
|
|
|
|
|
m_categoryView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
|
|
|
m_categoryView->setContextMenuPolicy(Qt::CustomContextMenu);
|
2023-10-13 12:28:52 +02:00
|
|
|
m_categoryView->setModel(m_sortFilterModel);
|
|
|
|
|
|
|
|
|
|
for (int i = LoggingCategoryModel::Column::Color; i < LoggingCategoryModel::Column::Info; i++)
|
|
|
|
|
m_categoryView->resizeColumnToContents(i);
|
|
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
auto filterEdit = new Utils::FancyLineEdit;
|
|
|
|
|
filterEdit->setHistoryCompleter("LogFilterCompletionHistory");
|
|
|
|
|
filterEdit->setFiltering(true);
|
|
|
|
|
filterEdit->setPlaceholderText(Tr::tr("Filter categories by regular expression"));
|
|
|
|
|
filterEdit->setText("^(?!qt\\.).+");
|
|
|
|
|
filterEdit->setValidationFunction(
|
|
|
|
|
[](const QString &input) {
|
|
|
|
|
return Utils::asyncRun([input]() -> Utils::expected_str<QString> {
|
|
|
|
|
QRegularExpression re(input);
|
|
|
|
|
if (re.isValid())
|
|
|
|
|
return input;
|
|
|
|
|
|
|
|
|
|
return Utils::make_unexpected(
|
|
|
|
|
Tr::tr("Invalid regular expression: %1").arg(re.errorString()));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
QSplitter *splitter{nullptr};
|
|
|
|
|
|
|
|
|
|
using namespace Layouting;
|
|
|
|
|
// clang-format off
|
|
|
|
|
Column {
|
|
|
|
|
Splitter {
|
|
|
|
|
bindTo(&splitter),
|
2023-10-16 13:15:17 +02:00
|
|
|
Column {
|
|
|
|
|
noMargin(),
|
|
|
|
|
Row {
|
|
|
|
|
spacing(0),
|
|
|
|
|
save,
|
|
|
|
|
clean,
|
|
|
|
|
m_stopLog,
|
|
|
|
|
autoScroll,
|
|
|
|
|
m_timestamps,
|
|
|
|
|
m_messageTypes,
|
|
|
|
|
st,
|
|
|
|
|
},
|
|
|
|
|
m_logView
|
|
|
|
|
},
|
|
|
|
|
Column {
|
|
|
|
|
noMargin(),
|
|
|
|
|
Row {
|
|
|
|
|
qtInternal,
|
|
|
|
|
filterEdit,
|
|
|
|
|
},
|
|
|
|
|
m_categoryView,
|
|
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
}
|
|
|
|
|
}.attachTo(this);
|
|
|
|
|
// clang-format on
|
|
|
|
|
|
|
|
|
|
splitter->setOrientation(Qt::Horizontal);
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
resize(800, 300);
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
connect(
|
|
|
|
|
&LoggingEntryModel::instance(),
|
|
|
|
|
&LoggingEntryModel::rowsInserted,
|
|
|
|
|
this,
|
|
|
|
|
[this, autoScroll] {
|
2022-01-17 08:07:08 +01:00
|
|
|
if (autoScroll->isChecked())
|
|
|
|
|
m_logView->scrollToBottom();
|
2023-10-13 12:28:52 +02:00
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
connect(m_categoryView,
|
|
|
|
|
&QAbstractItemView::activated,
|
|
|
|
|
m_sortFilterModel,
|
|
|
|
|
[this](const QModelIndex &index) {
|
|
|
|
|
const QVariant value = m_sortFilterModel->data(index, Qt::DecorationRole);
|
|
|
|
|
if (!value.isValid())
|
|
|
|
|
return;
|
|
|
|
|
const QColor original = value.value<QColor>();
|
|
|
|
|
if (!original.isValid())
|
|
|
|
|
return;
|
|
|
|
|
QColor changed = QColorDialog::getColor(original, this);
|
|
|
|
|
if (!changed.isValid())
|
|
|
|
|
return;
|
|
|
|
|
if (original != changed)
|
|
|
|
|
m_sortFilterModel->setData(index, changed, Qt::DecorationRole);
|
|
|
|
|
});
|
|
|
|
|
connect(save, &QToolButton::clicked, this, &LoggingViewManagerWidget::saveLoggingsToFile);
|
|
|
|
|
connect(m_logView,
|
|
|
|
|
&QAbstractItemView::customContextMenuRequested,
|
|
|
|
|
this,
|
|
|
|
|
&LoggingViewManagerWidget::showLogViewContextMenu);
|
|
|
|
|
connect(m_categoryView,
|
|
|
|
|
&QAbstractItemView::customContextMenuRequested,
|
|
|
|
|
this,
|
|
|
|
|
&LoggingViewManagerWidget::showLogCategoryContextMenu);
|
|
|
|
|
connect(clean,
|
|
|
|
|
&QToolButton::clicked,
|
|
|
|
|
&LoggingEntryModel::instance(),
|
|
|
|
|
&Utils::ListModel<LogEntry>::clear);
|
|
|
|
|
connect(m_stopLog, &QToolButton::toggled, this, [this](bool checked) {
|
|
|
|
|
LoggingEntryModel::instance().setEnabled(!checked);
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
m_stopLog->setIcon(Utils::Icons::RUN_SMALL.icon());
|
|
|
|
|
m_stopLog->setToolTip(Tr::tr("Start Logging"));
|
|
|
|
|
m_categoryModel->setUseOriginal(true);
|
|
|
|
|
} else {
|
|
|
|
|
m_stopLog->setIcon(Utils::Icons::STOP_SMALL.icon());
|
|
|
|
|
m_stopLog->setToolTip(Tr::tr("Stop Logging"));
|
|
|
|
|
m_categoryModel->setUseOriginal(false);
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
});
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
m_sortFilterModel->setFilterRegularExpression("^(?!qt\\.).+");
|
|
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
connect(qtInternal, &QToolButton::clicked, filterEdit, [filterEdit] {
|
|
|
|
|
filterEdit->setText("^(?!qt\\.).+");
|
2021-08-19 22:32:21 +02:00
|
|
|
});
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
connect(filterEdit,
|
|
|
|
|
&Utils::FancyLineEdit::textChanged,
|
|
|
|
|
m_sortFilterModel,
|
|
|
|
|
[this](const QString &f) {
|
|
|
|
|
QRegularExpression re(f);
|
|
|
|
|
if (re.isValid())
|
|
|
|
|
m_sortFilterModel->setFilterRegularExpression(f);
|
|
|
|
|
});
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
connect(m_timestamps, &QToolButton::toggled, this, [this](bool checked) {
|
2021-08-19 22:32:21 +02:00
|
|
|
m_logView->setColumnHidden(0, !checked);
|
|
|
|
|
});
|
2023-10-13 12:28:52 +02:00
|
|
|
connect(m_messageTypes, &QToolButton::toggled, this, [this](bool checked) {
|
2021-08-19 22:32:21 +02:00
|
|
|
m_logView->setColumnHidden(2, !checked);
|
|
|
|
|
});
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
ICore::registerWindow(this, Context("Qtc.LogViewer"));
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LoggingViewManagerWidget::showLogViewContextMenu(const QPoint &pos) const
|
|
|
|
|
{
|
|
|
|
|
QMenu m;
|
2023-01-16 17:20:07 +01:00
|
|
|
auto copy = new QAction(Tr::tr("Copy Selected Logs"), &m);
|
2021-08-19 22:32:21 +02:00
|
|
|
m.addAction(copy);
|
2023-01-16 17:20:07 +01:00
|
|
|
auto copyAll = new QAction(Tr::tr("Copy All"), &m);
|
2021-08-19 22:32:21 +02:00
|
|
|
m.addAction(copyAll);
|
2022-07-19 22:37:03 +02:00
|
|
|
connect(copy, &QAction::triggered, &m, [this] {
|
2021-08-19 22:32:21 +02:00
|
|
|
auto selectionModel = m_logView->selectionModel();
|
|
|
|
|
QString copied;
|
|
|
|
|
const bool useTS = m_timestamps->isChecked();
|
|
|
|
|
const bool useLL = m_messageTypes->isChecked();
|
2023-10-13 12:28:52 +02:00
|
|
|
for (int row = 0, end = LoggingEntryModel::instance().rowCount(); row < end; ++row) {
|
2021-08-19 22:32:21 +02:00
|
|
|
if (selectionModel->isRowSelected(row, QModelIndex()))
|
2023-10-13 12:28:52 +02:00
|
|
|
copied.append(LoggingEntryModel::instance().dataAt(row).outputLine(useTS, useLL));
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
Utils::setClipboardAndSelection(copied);
|
2021-08-19 22:32:21 +02:00
|
|
|
});
|
2022-07-19 22:37:03 +02:00
|
|
|
connect(copyAll, &QAction::triggered, &m, [this] {
|
2021-08-19 22:32:21 +02:00
|
|
|
QString copied;
|
|
|
|
|
const bool useTS = m_timestamps->isChecked();
|
|
|
|
|
const bool useLL = m_messageTypes->isChecked();
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
for (int row = 0, end = LoggingEntryModel::instance().rowCount(); row < end; ++row)
|
|
|
|
|
copied.append(LoggingEntryModel::instance().dataAt(row).outputLine(useTS, useLL));
|
2021-08-19 22:32:21 +02:00
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
Utils::setClipboardAndSelection(copied);
|
2021-08-19 22:32:21 +02:00
|
|
|
});
|
|
|
|
|
m.exec(m_logView->mapToGlobal(pos));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LoggingViewManagerWidget::showLogCategoryContextMenu(const QPoint &pos) const
|
|
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
QModelIndex idx = m_categoryView->indexAt(pos);
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
QMenu m;
|
2023-10-13 12:28:52 +02:00
|
|
|
auto uncheckAll = new QAction(Tr::tr("Uncheck All"), &m);
|
2023-10-16 13:15:17 +02:00
|
|
|
auto resetAll = new QAction(Tr::tr("Reset All"), &m);
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
auto isTypeColumn = [](int column) {
|
|
|
|
|
return column >= LoggingCategoryModel::Column::Debug
|
|
|
|
|
&& column <= LoggingCategoryModel::Column::Info;
|
|
|
|
|
};
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2023-10-16 13:15:17 +02:00
|
|
|
auto setChecked = [this](std::initializer_list<LoggingCategoryModel::Column> columns,
|
|
|
|
|
Qt::CheckState checked) {
|
|
|
|
|
for (int row = 0, count = m_sortFilterModel->rowCount(); row < count; ++row) {
|
|
|
|
|
for (int column : columns) {
|
|
|
|
|
m_sortFilterModel->setData(m_sortFilterModel->index(row, column),
|
|
|
|
|
checked,
|
|
|
|
|
Qt::CheckStateRole);
|
|
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
}
|
2023-10-16 13:15:17 +02:00
|
|
|
};
|
|
|
|
|
auto resetToOriginal = [this](std::initializer_list<LoggingCategoryModel::Column> columns) {
|
|
|
|
|
for (int row = 0, count = m_sortFilterModel->rowCount(); row < count; ++row) {
|
|
|
|
|
for (int column : columns) {
|
|
|
|
|
const QModelIndex id = m_sortFilterModel->index(row, column);
|
|
|
|
|
m_sortFilterModel->setData(id,
|
|
|
|
|
id.data(LoggingCategoryModel::OriginalStateRole),
|
|
|
|
|
Qt::CheckStateRole);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (idx.isValid() && isTypeColumn(idx.column())) {
|
|
|
|
|
const LoggingCategoryModel::Column column = static_cast<LoggingCategoryModel::Column>(
|
|
|
|
|
idx.column());
|
|
|
|
|
bool isChecked = idx.data(Qt::CheckStateRole).toInt() == Qt::Checked;
|
|
|
|
|
const QString uncheckText = isChecked ? Tr::tr("Uncheck All %1") : Tr::tr("Check All %1");
|
|
|
|
|
|
|
|
|
|
uncheckAll->setText(uncheckText.arg(messageTypeToString(
|
|
|
|
|
static_cast<QtMsgType>(column - LoggingCategoryModel::Column::Debug))));
|
|
|
|
|
resetAll->setText(Tr::tr("Reset All %1")
|
|
|
|
|
.arg(messageTypeToString(static_cast<QtMsgType>(
|
|
|
|
|
column - LoggingCategoryModel::Column::Debug))));
|
|
|
|
|
|
|
|
|
|
Qt::CheckState newState = isChecked ? Qt::Unchecked : Qt::Checked;
|
|
|
|
|
|
|
|
|
|
connect(uncheckAll,
|
|
|
|
|
&QAction::triggered,
|
|
|
|
|
m_sortFilterModel,
|
|
|
|
|
[setChecked, column, newState]() { setChecked({column}, newState); });
|
|
|
|
|
|
|
|
|
|
connect(resetAll, &QAction::triggered, m_sortFilterModel, [resetToOriginal, column]() {
|
|
|
|
|
resetToOriginal({column});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// No need to add Fatal here, as it is read-only
|
|
|
|
|
static auto allColumns = {LoggingCategoryModel::Column::Debug,
|
|
|
|
|
LoggingCategoryModel::Column::Warning,
|
|
|
|
|
LoggingCategoryModel::Column::Critical,
|
|
|
|
|
LoggingCategoryModel::Column::Info};
|
|
|
|
|
|
|
|
|
|
connect(uncheckAll, &QAction::triggered, m_sortFilterModel, [setChecked]() {
|
|
|
|
|
setChecked(allColumns, Qt::Unchecked);
|
|
|
|
|
});
|
|
|
|
|
connect(resetAll, &QAction::triggered, m_sortFilterModel, [resetToOriginal]() {
|
|
|
|
|
resetToOriginal(allColumns);
|
|
|
|
|
});
|
2023-10-13 12:28:52 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
// minimal load/save - plugins could later provide presets on their own?
|
2023-01-16 17:20:07 +01:00
|
|
|
auto savePreset = new QAction(Tr::tr("Save Enabled as Preset..."), &m);
|
2021-08-19 22:32:21 +02:00
|
|
|
m.addAction(savePreset);
|
2023-01-16 17:20:07 +01:00
|
|
|
auto loadPreset = new QAction(Tr::tr("Update from Preset..."), &m);
|
2021-08-19 22:32:21 +02:00
|
|
|
m.addAction(loadPreset);
|
|
|
|
|
m.addAction(uncheckAll);
|
2023-10-16 13:15:17 +02:00
|
|
|
m.addAction(resetAll);
|
2023-10-13 12:28:52 +02:00
|
|
|
connect(savePreset,
|
|
|
|
|
&QAction::triggered,
|
|
|
|
|
m_categoryModel,
|
|
|
|
|
&LoggingCategoryModel::saveEnabledCategoryPreset);
|
|
|
|
|
connect(loadPreset,
|
|
|
|
|
&QAction::triggered,
|
|
|
|
|
m_categoryModel,
|
|
|
|
|
&LoggingCategoryModel::loadAndUpdateFromPreset);
|
2021-08-19 22:32:21 +02:00
|
|
|
m.exec(m_categoryView->mapToGlobal(pos));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LoggingViewManagerWidget::saveLoggingsToFile() const
|
|
|
|
|
{
|
|
|
|
|
const Utils::FilePath fp = Utils::FileUtils::getSaveFilePath(ICore::dialogParent(),
|
2023-10-13 12:28:52 +02:00
|
|
|
Tr::tr("Save Logs As"),
|
|
|
|
|
{},
|
|
|
|
|
"*.log");
|
2021-08-19 22:32:21 +02:00
|
|
|
if (fp.isEmpty())
|
|
|
|
|
return;
|
2023-10-13 12:28:52 +02:00
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
const bool useTS = m_timestamps->isChecked();
|
|
|
|
|
const bool useLL = m_messageTypes->isChecked();
|
|
|
|
|
QFile file(fp.path());
|
|
|
|
|
if (file.open(QIODevice::WriteOnly)) {
|
2023-10-13 12:28:52 +02:00
|
|
|
for (int row = 0, end = LoggingEntryModel::instance().rowCount(); row < end; ++row) {
|
|
|
|
|
qint64 res = file.write(
|
|
|
|
|
LoggingEntryModel::instance().dataAt(row).outputLine(useTS, useLL).toUtf8());
|
2021-08-19 22:32:21 +02:00
|
|
|
if (res == -1) {
|
2023-10-13 12:28:52 +02:00
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
|
|
|
|
Tr::tr("Error"),
|
|
|
|
|
Tr::tr("Failed to write logs to \"%1\".")
|
|
|
|
|
.arg(fp.toUserOutput()));
|
2021-08-19 22:32:21 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
} else {
|
2023-10-13 12:28:52 +02:00
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
|
|
|
|
Tr::tr("Error"),
|
|
|
|
|
Tr::tr("Failed to open file \"%1\" for writing logs.")
|
|
|
|
|
.arg(fp.toUserOutput()));
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void LoggingCategoryModel::saveEnabledCategoryPreset() const
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
|
|
|
|
Utils::FilePath fp = Utils::FileUtils::getSaveFilePath(ICore::dialogParent(),
|
2023-10-13 12:28:52 +02:00
|
|
|
Tr::tr("Save Enabled Categories As..."),
|
|
|
|
|
{},
|
|
|
|
|
"*.json");
|
2021-08-19 22:32:21 +02:00
|
|
|
if (fp.isEmpty())
|
|
|
|
|
return;
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
auto minLevel = [](const LoggingCategoryEntry &logCategory) {
|
|
|
|
|
for (int i = QtDebugMsg; i <= QtInfoMsg; i++) {
|
|
|
|
|
if (logCategory.isEnabled(static_cast<QtMsgType>(i)))
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return QtInfoMsg + 1;
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
QJsonArray array;
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
for (auto item : m_categories) {
|
2021-08-19 22:32:21 +02:00
|
|
|
QJsonObject itemObj;
|
2023-10-13 12:28:52 +02:00
|
|
|
itemObj.insert("name", item.name());
|
2021-08-19 22:32:21 +02:00
|
|
|
QJsonObject entryObj;
|
2023-10-13 12:28:52 +02:00
|
|
|
entryObj.insert("level", minLevel(item));
|
|
|
|
|
if (item.color().isValid())
|
|
|
|
|
entryObj.insert("color", item.color().name(QColor::HexArgb));
|
|
|
|
|
|
|
|
|
|
QVariantMap levels = {{"Debug", item.isDebugEnabled()},
|
|
|
|
|
{"Warning", item.isWarningEnabled()},
|
|
|
|
|
{"Critical", item.isCriticalEnabled()},
|
|
|
|
|
{"Info", item.isInfoEnabled()}};
|
|
|
|
|
entryObj.insert("levels", QJsonValue::fromVariant(levels));
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
itemObj.insert("entry", entryObj);
|
|
|
|
|
array.append(itemObj);
|
|
|
|
|
}
|
|
|
|
|
QJsonDocument doc(array);
|
|
|
|
|
if (!fp.writeFileContents(doc.toJson(QJsonDocument::Compact)))
|
2023-10-13 12:28:52 +02:00
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
|
|
|
|
Tr::tr("Error"),
|
|
|
|
|
Tr::tr("Failed to write preset file \"%1\".").arg(fp.toUserOutput()));
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void LoggingCategoryModel::loadAndUpdateFromPreset()
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
|
|
|
|
Utils::FilePath fp = Utils::FileUtils::getOpenFilePath(ICore::dialogParent(),
|
2023-01-16 17:20:07 +01:00
|
|
|
Tr::tr("Load Enabled Categories From"));
|
2021-08-19 22:32:21 +02:00
|
|
|
if (fp.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
// read file, update categories
|
2022-11-24 08:52:47 +01:00
|
|
|
const Utils::expected_str<QByteArray> contents = fp.fileContents();
|
2022-09-09 13:48:08 +02:00
|
|
|
if (!contents) {
|
|
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
2023-01-16 17:20:07 +01:00
|
|
|
Tr::tr("Error"),
|
2023-05-26 11:52:43 +02:00
|
|
|
Tr::tr("Failed to open preset file \"%1\" for reading.")
|
2022-09-09 13:48:08 +02:00
|
|
|
.arg(fp.toUserOutput()));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
QJsonParseError error;
|
2022-09-09 13:48:08 +02:00
|
|
|
QJsonDocument doc = QJsonDocument::fromJson(*contents, &error);
|
2021-08-19 22:32:21 +02:00
|
|
|
if (error.error != QJsonParseError::NoError) {
|
2023-10-13 12:28:52 +02:00
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
|
|
|
|
Tr::tr("Error"),
|
|
|
|
|
Tr::tr("Failed to read preset file \"%1\": %2")
|
|
|
|
|
.arg(fp.toUserOutput())
|
|
|
|
|
.arg(error.errorString()));
|
2021-08-19 22:32:21 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
bool formatError = false;
|
2023-10-13 12:28:52 +02:00
|
|
|
QList<SavedEntry> presetItems;
|
2021-08-19 22:32:21 +02:00
|
|
|
if (doc.isArray()) {
|
|
|
|
|
const QJsonArray array = doc.array();
|
|
|
|
|
for (const QJsonValue &value : array) {
|
|
|
|
|
if (!value.isObject()) {
|
|
|
|
|
formatError = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const QJsonObject itemObj = value.toObject();
|
2023-10-13 12:28:52 +02:00
|
|
|
Utils::expected_str<SavedEntry> item = SavedEntry::fromJson(itemObj);
|
|
|
|
|
if (!item) {
|
2021-08-19 22:32:21 +02:00
|
|
|
formatError = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
presetItems.append(*item);
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
formatError = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (formatError) {
|
2023-10-13 12:28:52 +02:00
|
|
|
QMessageBox::critical(ICore::dialogParent(),
|
|
|
|
|
Tr::tr("Error"),
|
2023-01-16 17:20:07 +01:00
|
|
|
Tr::tr("Unexpected preset file format."));
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
2023-10-13 12:28:52 +02:00
|
|
|
|
|
|
|
|
int idx = 0;
|
|
|
|
|
|
|
|
|
|
for (auto it = presetItems.begin(); it != presetItems.end(); ++it, ++idx) {
|
|
|
|
|
QList<LoggingCategoryEntry>::iterator itExisting
|
|
|
|
|
= std::find_if(m_categories.begin(),
|
|
|
|
|
m_categories.end(),
|
|
|
|
|
[e = *it](const LoggingCategoryEntry &cat) {
|
|
|
|
|
return cat.name() == e.name;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (it->color.isValid())
|
|
|
|
|
setCategoryColor(it->name, it->color);
|
|
|
|
|
|
|
|
|
|
if (itExisting != m_categories.end()) {
|
|
|
|
|
itExisting->setSaved(*it);
|
|
|
|
|
emit dataChanged(createIndex(idx, Column::Color), createIndex(idx, Column::Info));
|
|
|
|
|
} else {
|
|
|
|
|
LoggingCategoryEntry newEntry(*it);
|
|
|
|
|
append(newEntry);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
QColor colorForCategory(const QString &category)
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
auto entry = s_categoryColor.find(category);
|
|
|
|
|
if (entry == s_categoryColor.end())
|
2021-08-19 22:32:21 +02:00
|
|
|
return Utils::creatorTheme()->palette().text().color();
|
|
|
|
|
return entry.value();
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
void setCategoryColor(const QString &category, const QColor &color)
|
2021-08-19 22:32:21 +02:00
|
|
|
{
|
|
|
|
|
const QColor baseColor = Utils::creatorTheme()->palette().text().color();
|
|
|
|
|
if (color != baseColor)
|
2023-10-13 12:28:52 +02:00
|
|
|
s_categoryColor.insert(category, color);
|
2021-08-19 22:32:21 +02:00
|
|
|
else
|
2023-10-13 12:28:52 +02:00
|
|
|
s_categoryColor.remove(category);
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-30 08:11:11 +01:00
|
|
|
static bool wasLogViewerShown = false;
|
|
|
|
|
|
2021-08-19 22:32:21 +02:00
|
|
|
void LoggingViewer::showLoggingView()
|
|
|
|
|
{
|
2023-10-13 12:28:52 +02:00
|
|
|
LoggingViewManagerWidget *staticLogWidget = LoggingViewManagerWidget::instance();
|
|
|
|
|
QTC_ASSERT(staticLogWidget, return);
|
|
|
|
|
|
|
|
|
|
staticLogWidget->show();
|
|
|
|
|
staticLogWidget->raise();
|
|
|
|
|
staticLogWidget->activateWindow();
|
2023-10-30 08:11:11 +01:00
|
|
|
|
|
|
|
|
wasLogViewerShown = true;
|
2023-10-13 12:28:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LoggingViewer::hideLoggingView()
|
|
|
|
|
{
|
2023-10-30 08:11:11 +01:00
|
|
|
if (!wasLogViewerShown)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
LoggingViewManagerWidget *staticLogWidget = LoggingViewManagerWidget::instance();
|
|
|
|
|
QTC_ASSERT(staticLogWidget, return);
|
|
|
|
|
staticLogWidget->close();
|
|
|
|
|
delete staticLogWidget;
|
2021-08-19 22:32:21 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-13 12:28:52 +02:00
|
|
|
} // namespace Core::Internal
|
2021-08-19 22:32:21 +02:00
|
|
|
|
|
|
|
|
#include "loggingviewer.moc"
|