forked from qt-creator/qt-creator
Core: Add LoggingView support
Add a way to inspect QC internal loggings. This is basically useful for inspecting issues while running QC and facing them without the need to restart and set appropriate logging rules. Change-Id: Ic647ba1abfb2611c4e4e99a375413d399c71886d Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
BIN
src/libs/utils/images/message.png
Normal file
BIN
src/libs/utils/images/message.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 379 B |
BIN
src/libs/utils/images/message@2x.png
Normal file
BIN
src/libs/utils/images/message@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 745 B |
@@ -235,6 +235,8 @@
|
||||
<file>images/online@2x.png</file>
|
||||
<file>images/download.png</file>
|
||||
<file>images/download@2x.png</file>
|
||||
<file>images/message.png</file>
|
||||
<file>images/message@2x.png</file>
|
||||
<file alias="mimetypes/freedesktop.org.xml" compression-algorithm="best">../3rdparty/xdg/freedesktop.org.xml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/codemodel">
|
||||
|
@@ -119,6 +119,8 @@ add_qtc_plugin(Core
|
||||
locator/opendocumentsfilter.cpp locator/opendocumentsfilter.h
|
||||
locator/spotlightlocatorfilter.h locator/spotlightlocatorfilter.cpp
|
||||
locator/urllocatorfilter.cpp locator/urllocatorfilter.h locator/urllocatorfilter.ui
|
||||
loggingviewer.cpp loggingviewer.h
|
||||
loggingmanager.cpp loggingmanager.h
|
||||
mainwindow.cpp mainwindow.h
|
||||
manhattanstyle.cpp manhattanstyle.h
|
||||
menubarfilter.cpp menubarfilter.h
|
||||
|
@@ -1,10 +1,12 @@
|
||||
<RCC>
|
||||
<qresource prefix="/core">
|
||||
<file alias="images/qtcreatorlogo-big.png">images/logo/128/QtProject-qtcreator.png</file>
|
||||
<file alias="images/qtcreatorlogo-big@2x.png">images/logo/256/QtProject-qtcreator.png</file>
|
||||
<file>images/settingscategory_core.png</file>
|
||||
<file>images/settingscategory_core@2x.png</file>
|
||||
<file>images/settingscategory_design.png</file>
|
||||
<file>images/settingscategory_design@2x.png</file>
|
||||
<file alias="images/qtcreatorlogo-big.png">images/logo/128/QtProject-qtcreator.png</file>
|
||||
<file alias="images/qtcreatorlogo-big@2x.png">images/logo/256/QtProject-qtcreator.png</file>
|
||||
<file>images/settingscategory_core.png</file>
|
||||
<file>images/settingscategory_core@2x.png</file>
|
||||
<file>images/settingscategory_design.png</file>
|
||||
<file>images/settingscategory_design@2x.png</file>
|
||||
<file alias="images/qtlogo.png">images/logo/16/Qt_logo_green.png</file>
|
||||
<file alias="images/qtlogo@2x.png">images/logo/32/Qt_logo_green.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@@ -98,6 +98,7 @@ const char PRINT[] = "QtCreator.Print";
|
||||
const char EXIT[] = "QtCreator.Exit";
|
||||
|
||||
const char OPTIONS[] = "QtCreator.Options";
|
||||
const char LOGGER[] = "QtCreator.Logger";
|
||||
const char TOGGLE_LEFT_SIDEBAR[] = "QtCreator.ToggleLeftSidebar";
|
||||
const char TOGGLE_RIGHT_SIDEBAR[] = "QtCreator.ToggleRightSidebar";
|
||||
const char CYCLE_MODE_SELECTOR_STYLE[] =
|
||||
|
@@ -32,6 +32,8 @@ namespace Icons {
|
||||
|
||||
const Icon QTCREATORLOGO_BIG(
|
||||
":/core/images/qtcreatorlogo-big.png");
|
||||
const Icon QTLOGO(
|
||||
":/core/images/qtlogo.png");
|
||||
const Icon FIND_CASE_INSENSITIVELY(
|
||||
":/find/images/casesensitively.png");
|
||||
const Icon FIND_WHOLE_WORD(
|
||||
|
@@ -33,6 +33,7 @@ namespace Core {
|
||||
namespace Icons {
|
||||
|
||||
CORE_EXPORT extern const Utils::Icon QTCREATORLOGO_BIG;
|
||||
CORE_EXPORT extern const Utils::Icon QTLOGO;
|
||||
CORE_EXPORT extern const Utils::Icon FIND_CASE_INSENSITIVELY;
|
||||
CORE_EXPORT extern const Utils::Icon FIND_WHOLE_WORD;
|
||||
CORE_EXPORT extern const Utils::Icon FIND_REGEXP;
|
||||
|
@@ -12,6 +12,8 @@ isEmpty(QTC_SHOW_BUILD_DATE): QTC_SHOW_BUILD_DATE = $$(QTC_SHOW_BUILD_DATE)
|
||||
include(../../qtcreatorplugin.pri)
|
||||
msvc: QMAKE_CXXFLAGS += -wd4251 -wd4290 -wd4250
|
||||
SOURCES += corejsextensions.cpp \
|
||||
loggingmanager.cpp \
|
||||
loggingviewer.cpp \
|
||||
mainwindow.cpp \
|
||||
shellcommand.cpp \
|
||||
editmode.cpp \
|
||||
@@ -115,6 +117,8 @@ SOURCES += corejsextensions.cpp \
|
||||
foldernavigationwidget.cpp
|
||||
|
||||
HEADERS += corejsextensions.h \
|
||||
loggingmanager.h \
|
||||
loggingviewer.h \
|
||||
mainwindow.h \
|
||||
shellcommand.h \
|
||||
editmode.h \
|
||||
|
@@ -106,6 +106,10 @@ Project {
|
||||
"iwizardfactory.h",
|
||||
"jsexpander.cpp",
|
||||
"jsexpander.h",
|
||||
"loggingmanager.cpp",
|
||||
"loggingmanager.h",
|
||||
"loggingviewer.cpp",
|
||||
"loggingviewer.h",
|
||||
"mainwindow.cpp",
|
||||
"mainwindow.h",
|
||||
"manhattanstyle.cpp",
|
||||
|
BIN
src/plugins/coreplugin/images/logo/16/Qt_logo_green.png
Normal file
BIN
src/plugins/coreplugin/images/logo/16/Qt_logo_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 404 B |
BIN
src/plugins/coreplugin/images/logo/32/Qt_logo_green.png
Normal file
BIN
src/plugins/coreplugin/images/logo/32/Qt_logo_green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 681 B |
323
src/plugins/coreplugin/loggingmanager.cpp
Normal file
323
src/plugins/coreplugin/loggingmanager.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "loggingmanager.h"
|
||||
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QLibraryInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
//
|
||||
// WARNING! Do not use qDebug(), qWarning() or similar inside this file -
|
||||
// same applies for indirect usages (e.g. QTC_ASSERT() and the like).
|
||||
// Using static functions of QLoggingCategory may cause dead locks as well.
|
||||
//
|
||||
|
||||
namespace Core {
|
||||
namespace Internal {
|
||||
|
||||
static QtMessageHandler s_originalMessageHandler = nullptr;
|
||||
|
||||
static LoggingViewManager *s_instance = nullptr;
|
||||
|
||||
static QString levelToString(QtMsgType t)
|
||||
{
|
||||
switch (t) {
|
||||
case QtMsgType::QtCriticalMsg: return {"critical"};
|
||||
case QtMsgType::QtDebugMsg: return {"debug"};
|
||||
case QtMsgType::QtInfoMsg: return {"info"};
|
||||
case QtMsgType::QtWarningMsg: return {"warning"};
|
||||
default:
|
||||
return {"fatal"}; // wrong but we don't care
|
||||
}
|
||||
}
|
||||
|
||||
static QtMsgType parseLevel(const QString &level)
|
||||
{
|
||||
switch (level.at(0).toLatin1()) {
|
||||
case 'c': return QtMsgType::QtCriticalMsg;
|
||||
case 'd': return QtMsgType::QtDebugMsg;
|
||||
case 'i': return QtMsgType::QtInfoMsg;
|
||||
case 'w': return QtMsgType::QtWarningMsg;
|
||||
default:
|
||||
return QtMsgType::QtFatalMsg; // wrong but we don't care
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseLine(const QString &line, FilterRuleSpec *filterRule)
|
||||
{
|
||||
const QStringList parts = line.split('=');
|
||||
if (parts.size() != 2)
|
||||
return false;
|
||||
|
||||
const QString category = parts.at(0);
|
||||
static const QRegularExpression regex("^(.+?)(\\.(debug|info|warning|critical))?$");
|
||||
const QRegularExpressionMatch match = regex.match(category);
|
||||
if (!match.hasMatch())
|
||||
return false;
|
||||
|
||||
const QString categoryName = match.captured(1);
|
||||
if (categoryName.size() > 2) {
|
||||
if (categoryName.mid(1, categoryName.size() - 2).contains('*'))
|
||||
return false;
|
||||
} else if (categoryName.size() == 2) {
|
||||
if (categoryName.count('*') == 2)
|
||||
return false;
|
||||
}
|
||||
filterRule->category = categoryName;
|
||||
|
||||
if (match.capturedLength(2) == 0)
|
||||
filterRule->level = Utils::nullopt;
|
||||
else
|
||||
filterRule->level = Utils::make_optional(parseLevel(match.captured(2).mid(1)));
|
||||
|
||||
const QString enabled = parts.at(1);
|
||||
if (enabled == "true" || enabled == "false") {
|
||||
filterRule->enabled = (enabled == "true");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static QList<FilterRuleSpec> fetchOriginalRules()
|
||||
{
|
||||
QList<FilterRuleSpec> rules;
|
||||
|
||||
auto appendRulesFromFile = [&rules](const QString &fileName) {
|
||||
QSettings iniSettings(fileName, QSettings::IniFormat);
|
||||
iniSettings.beginGroup("Rules");
|
||||
const QStringList keys = iniSettings.allKeys();
|
||||
for (const QString &key : keys) {
|
||||
const QString value = iniSettings.value(key).toString();
|
||||
FilterRuleSpec filterRule;
|
||||
if (parseLine(key + "=" + value, &filterRule))
|
||||
rules.append(filterRule);
|
||||
}
|
||||
iniSettings.endGroup();
|
||||
};
|
||||
|
||||
Utils::FilePath iniFile = Utils::FilePath::fromString(
|
||||
QLibraryInfo::location(QLibraryInfo::DataPath)).pathAppended("qtlogging.ini");
|
||||
if (iniFile.exists())
|
||||
appendRulesFromFile(iniFile.toString());
|
||||
|
||||
const QString qtProjectString = QStandardPaths::locate(QStandardPaths::GenericConfigLocation,
|
||||
"QtProject/qtlogging.ini");
|
||||
if (!qtProjectString.isEmpty())
|
||||
appendRulesFromFile(qtProjectString);
|
||||
|
||||
iniFile = Utils::FilePath::fromString(qEnvironmentVariable("QT_LOGGING_CONF"));
|
||||
if (iniFile.exists())
|
||||
appendRulesFromFile(iniFile.toString());
|
||||
|
||||
if (qEnvironmentVariableIsSet("QT_LOGGING_RULES")) {
|
||||
const QStringList rulesStrings = qEnvironmentVariable("QT_LOGGING_RULES").split(';');
|
||||
for (const QString &rule : rulesStrings) {
|
||||
FilterRuleSpec filterRule;
|
||||
if (parseLine(rule, &filterRule))
|
||||
rules.append(filterRule);
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
LoggingViewManager::LoggingViewManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_originalLoggingRules(qEnvironmentVariable("QT_LOGGING_RULES"))
|
||||
{
|
||||
qRegisterMetaType<Core::Internal::LoggingCategoryEntry>();
|
||||
s_instance = this;
|
||||
s_originalMessageHandler = qInstallMessageHandler(logMessageHandler);
|
||||
m_enabled = true;
|
||||
m_originalRules = fetchOriginalRules();
|
||||
prefillCategories();
|
||||
QLoggingCategory::setFilterRules("*=true");
|
||||
}
|
||||
|
||||
LoggingViewManager::~LoggingViewManager()
|
||||
{
|
||||
m_enabled = false;
|
||||
qInstallMessageHandler(s_originalMessageHandler);
|
||||
s_originalMessageHandler = nullptr;
|
||||
qputenv("QT_LOGGING_RULES", m_originalLoggingRules.toLocal8Bit());
|
||||
QLoggingCategory::setFilterRules("*=false");
|
||||
resetFilterRules();
|
||||
s_instance = nullptr;
|
||||
}
|
||||
|
||||
LoggingViewManager *LoggingViewManager::instance()
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void LoggingViewManager::logMessageHandler(QtMsgType type, const QMessageLogContext &context,
|
||||
const QString &mssg)
|
||||
{
|
||||
if (!s_instance->m_enabled) {
|
||||
if (s_instance->enabledInOriginalRules(context, type))
|
||||
s_originalMessageHandler(type, context, mssg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.category) {
|
||||
s_originalMessageHandler(type, context, mssg);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString category = QString::fromLocal8Bit(context.category);
|
||||
auto it = s_instance->m_categories.find(category);
|
||||
if (it == s_instance->m_categories.end()) {
|
||||
if (!s_instance->m_listQtInternal && category.startsWith("qt."))
|
||||
return;
|
||||
LoggingCategoryEntry entry;
|
||||
entry.level = QtMsgType::QtDebugMsg;
|
||||
entry.enabled = (category == "default") || s_instance->enabledInOriginalRules(context, type);
|
||||
it = s_instance->m_categories.insert(category, entry);
|
||||
emit s_instance->foundNewCategory(category, entry);
|
||||
}
|
||||
|
||||
const LoggingCategoryEntry entry = it.value();
|
||||
if (entry.enabled && enabled(type, entry.level)) {
|
||||
const QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
|
||||
emit s_instance->receivedLog(timestamp, category,
|
||||
LoggingViewManager::messageTypeToString(type), mssg);
|
||||
}
|
||||
}
|
||||
|
||||
bool LoggingViewManager::isCategoryEnabled(const QString &category)
|
||||
{
|
||||
auto entry = m_categories.find(category);
|
||||
if (entry == m_categories.end()) // shall not happen - paranoia
|
||||
return false;
|
||||
|
||||
return entry.value().enabled;
|
||||
}
|
||||
|
||||
void LoggingViewManager::setCategoryEnabled(const QString &category, bool enabled)
|
||||
{
|
||||
auto entry = m_categories.find(category);
|
||||
if (entry == m_categories.end()) // shall not happen - paranoia
|
||||
return;
|
||||
|
||||
entry->enabled = enabled;
|
||||
}
|
||||
|
||||
void LoggingViewManager::setLogLevel(const QString &category, QtMsgType type)
|
||||
{
|
||||
auto entry = m_categories.find(category);
|
||||
if (entry == m_categories.end()) // shall not happen - paranoia
|
||||
return;
|
||||
|
||||
entry->level = type;
|
||||
}
|
||||
|
||||
void LoggingViewManager::setListQtInternal(bool listQtInternal)
|
||||
{
|
||||
m_listQtInternal = listQtInternal;
|
||||
}
|
||||
|
||||
void LoggingViewManager::appendOrUpdate(const QString &category, const LoggingCategoryEntry &entry)
|
||||
{
|
||||
auto it = m_categories.find(category);
|
||||
bool append = it == m_categories.end();
|
||||
m_categories.insert(category, entry);
|
||||
if (append)
|
||||
emit foundNewCategory(category, entry);
|
||||
else
|
||||
emit updatedCategory(category, entry);
|
||||
}
|
||||
|
||||
/*
|
||||
* Does not check categories for being present, will perform early exit if m_categories is not empty
|
||||
*/
|
||||
void LoggingViewManager::prefillCategories()
|
||||
{
|
||||
if (!m_categories.isEmpty())
|
||||
return;
|
||||
|
||||
for (int i = 0, end = m_originalRules.size(); i < end; ++i) {
|
||||
const FilterRuleSpec &rule = m_originalRules.at(i);
|
||||
if (rule.category.startsWith('*') || rule.category.endsWith('*'))
|
||||
continue;
|
||||
|
||||
bool enabled = rule.enabled;
|
||||
// check following rules whether they might overwrite
|
||||
for (int j = i + 1; j < end; ++j) {
|
||||
const FilterRuleSpec &secondRule = m_originalRules.at(j);
|
||||
const QRegularExpression regex(
|
||||
QRegularExpression::wildcardToRegularExpression(secondRule.category));
|
||||
if (!regex.match(rule.category).hasMatch())
|
||||
continue;
|
||||
|
||||
if (secondRule.level.has_value() && rule.level != secondRule.level)
|
||||
continue;
|
||||
|
||||
enabled = secondRule.enabled;
|
||||
}
|
||||
LoggingCategoryEntry entry;
|
||||
entry.level = rule.level.value_or(QtMsgType::QtInfoMsg);
|
||||
entry.enabled = enabled;
|
||||
m_categories.insert(rule.category, entry);
|
||||
}
|
||||
}
|
||||
|
||||
void LoggingViewManager::resetFilterRules()
|
||||
{
|
||||
for (const FilterRuleSpec &rule : qAsConst(m_originalRules)) {
|
||||
const QString level = rule.level.has_value() ? '.' + levelToString(rule.level.value())
|
||||
: QString();
|
||||
const QString ruleString = rule.category + level + '=' + (rule.enabled ? "true" : "false");
|
||||
QLoggingCategory::setFilterRules(ruleString);
|
||||
}
|
||||
}
|
||||
|
||||
bool LoggingViewManager::enabledInOriginalRules(const QMessageLogContext &context, QtMsgType type)
|
||||
{
|
||||
if (!context.category)
|
||||
return false;
|
||||
const QString category = QString::fromUtf8(context.category);
|
||||
bool result = false;
|
||||
for (const FilterRuleSpec &rule : qAsConst(m_originalRules)) {
|
||||
const QRegularExpression regex(
|
||||
QRegularExpression::wildcardToRegularExpression(rule.category));
|
||||
if (regex.match(category).hasMatch()) {
|
||||
if (rule.level.has_value()) {
|
||||
if (rule.level.value() == type)
|
||||
result = rule.enabled;
|
||||
} else {
|
||||
result = rule.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Core
|
143
src/plugins/coreplugin/loggingmanager.h
Normal file
143
src/plugins/coreplugin/loggingmanager.h
Normal file
@@ -0,0 +1,143 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/optional.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
namespace Core {
|
||||
namespace Internal {
|
||||
|
||||
struct FilterRuleSpec
|
||||
{
|
||||
QString category;
|
||||
Utils::optional<QtMsgType> level;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
class LoggingCategoryEntry
|
||||
{
|
||||
public:
|
||||
QtMsgType level = QtDebugMsg;
|
||||
bool enabled = false;
|
||||
QColor color;
|
||||
};
|
||||
|
||||
class LoggingViewManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static inline QString messageTypeToString(QtMsgType type)
|
||||
{
|
||||
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"};
|
||||
}
|
||||
}
|
||||
|
||||
static inline QtMsgType messageTypeFromString(const QString &type)
|
||||
{
|
||||
if (type.isEmpty())
|
||||
return QtDebugMsg;
|
||||
|
||||
// shortcut - only handle expected
|
||||
switch (type.at(0).toLatin1()) {
|
||||
case 'I':
|
||||
return QtInfoMsg;
|
||||
case 'C':
|
||||
return QtCriticalMsg;
|
||||
case 'W':
|
||||
return QtWarningMsg;
|
||||
case 'D':
|
||||
default:
|
||||
return QtDebugMsg;
|
||||
}
|
||||
}
|
||||
|
||||
explicit LoggingViewManager(QObject *parent = nullptr);
|
||||
~LoggingViewManager();
|
||||
|
||||
static LoggingViewManager *instance();
|
||||
|
||||
static inline bool enabled(QtMsgType current, QtMsgType stored)
|
||||
{
|
||||
if (stored == QtMsgType::QtInfoMsg)
|
||||
return true;
|
||||
if (current == stored)
|
||||
return true;
|
||||
if (stored == QtMsgType::QtDebugMsg)
|
||||
return current != QtMsgType::QtInfoMsg;
|
||||
if (stored == QtMsgType::QtWarningMsg)
|
||||
return current == QtMsgType::QtCriticalMsg || current == QtMsgType::QtFatalMsg;
|
||||
if (stored == QtMsgType::QtCriticalMsg)
|
||||
return current == QtMsgType::QtFatalMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void logMessageHandler(QtMsgType type, const QMessageLogContext &context,
|
||||
const QString &mssg);
|
||||
|
||||
void setEnabled(bool enabled) { m_enabled = enabled; }
|
||||
bool isEnabled() const { return m_enabled; }
|
||||
bool isCategoryEnabled(const QString &category);
|
||||
void setCategoryEnabled(const QString &category, bool enabled);
|
||||
void setLogLevel(const QString &category, QtMsgType type);
|
||||
void setListQtInternal(bool listQtInternal);
|
||||
QList<FilterRuleSpec> originalRules() const { return m_originalRules; }
|
||||
|
||||
QMap<QString, LoggingCategoryEntry> categories() const { return m_categories; }
|
||||
void appendOrUpdate(const QString &category, const LoggingCategoryEntry &entry);
|
||||
|
||||
signals:
|
||||
void receivedLog(const QString ×tamp, const QString &type, const QString &category,
|
||||
const QString &msg);
|
||||
void foundNewCategory(const QString &category, const LoggingCategoryEntry &entry);
|
||||
void updatedCategory(const QString &category, const LoggingCategoryEntry &entry);
|
||||
|
||||
private:
|
||||
void prefillCategories();
|
||||
void resetFilterRules();
|
||||
bool enabledInOriginalRules(const QMessageLogContext &context, QtMsgType type);
|
||||
|
||||
QMap<QString, LoggingCategoryEntry> m_categories;
|
||||
const QString m_originalLoggingRules;
|
||||
QList<FilterRuleSpec> m_originalRules;
|
||||
bool m_enabled = false;
|
||||
bool m_listQtInternal = false;
|
||||
};
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Core
|
||||
|
||||
Q_DECLARE_METATYPE(Core::Internal::LoggingCategoryEntry)
|
740
src/plugins/coreplugin/loggingviewer.cpp
Normal file
740
src/plugins/coreplugin/loggingviewer.cpp
Normal file
@@ -0,0 +1,740 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "loggingviewer.h"
|
||||
|
||||
#include "actionmanager/actionmanager.h"
|
||||
#include "coreicons.h"
|
||||
#include "icore.h"
|
||||
#include "loggingmanager.h"
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/basetreeview.h>
|
||||
#include <utils/executeondestruction.h>
|
||||
#include <utils/listmodel.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QClipboard>
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace Core {
|
||||
namespace Internal {
|
||||
|
||||
class LoggingCategoryItem
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
LoggingCategoryEntry entry;
|
||||
|
||||
static LoggingCategoryItem fromJson(const QJsonObject &object, bool *ok);
|
||||
};
|
||||
|
||||
LoggingCategoryItem LoggingCategoryItem::fromJson(const QJsonObject &object, bool *ok)
|
||||
{
|
||||
if (!object.contains("name")) {
|
||||
*ok = false;
|
||||
return {};
|
||||
}
|
||||
const QJsonValue entryVal = object.value("entry");
|
||||
if (entryVal.isUndefined()) {
|
||||
*ok = false;
|
||||
return {};
|
||||
}
|
||||
const QJsonObject entryObj = entryVal.toObject();
|
||||
if (!entryObj.contains("level")) {
|
||||
*ok = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
LoggingCategoryEntry entry;
|
||||
entry.level = QtMsgType(entryObj.value("level").toInt());
|
||||
entry.enabled = true;
|
||||
if (entryObj.contains("color"))
|
||||
entry.color = QColor(entryObj.value("color").toString());
|
||||
LoggingCategoryItem item {object.value("name").toString(), entry};
|
||||
*ok = true;
|
||||
return item;
|
||||
}
|
||||
|
||||
class LoggingCategoryModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LoggingCategoryModel() = default;
|
||||
~LoggingCategoryModel() override;
|
||||
|
||||
bool append(const QString &category, const LoggingCategoryEntry &entry = {});
|
||||
bool update(const QString &category, const LoggingCategoryEntry &entry);
|
||||
int columnCount(const QModelIndex &) const final { return 3; }
|
||||
int rowCount(const QModelIndex & = QModelIndex()) const final { return m_categories.count(); }
|
||||
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;
|
||||
QVariant headerData(int section, Qt::Orientation orientation,
|
||||
int role = Qt::DisplayRole) const final;
|
||||
void reset();
|
||||
void setFromManager(LoggingViewManager *manager);
|
||||
QList<LoggingCategoryItem> enabledCategories() const;
|
||||
void disableAll();
|
||||
|
||||
signals:
|
||||
void categoryChanged(const QString &category, bool enabled);
|
||||
void colorChanged(const QString &category, const QColor &color);
|
||||
void logLevelChanged(const QString &category, QtMsgType logLevel);
|
||||
|
||||
private:
|
||||
QList<LoggingCategoryItem *> m_categories;
|
||||
};
|
||||
|
||||
LoggingCategoryModel::~LoggingCategoryModel()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
bool LoggingCategoryModel::append(const QString &category, const LoggingCategoryEntry &entry)
|
||||
{
|
||||
// no check?
|
||||
beginInsertRows(QModelIndex(), m_categories.size(), m_categories.size());
|
||||
m_categories.append(new LoggingCategoryItem{category, entry});
|
||||
endInsertRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoggingCategoryModel::update(const QString &category, const LoggingCategoryEntry &entry)
|
||||
{
|
||||
if (m_categories.size() == 0) // should not happen
|
||||
return false;
|
||||
|
||||
int row = 0;
|
||||
for (int end = m_categories.size(); row < end; ++row) {
|
||||
if (m_categories.at(row)->name == category)
|
||||
break;
|
||||
}
|
||||
if (row == m_categories.size()) // should not happen
|
||||
return false;
|
||||
|
||||
setData(index(row, 0), Qt::Checked, Qt::CheckStateRole);
|
||||
setData(index(row, 1), LoggingViewManager::messageTypeToString(entry.level), Qt::EditRole);
|
||||
setData(index(row, 2), entry.color, Qt::DecorationRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant LoggingCategoryModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
static const QColor defaultColor = Utils::creatorTheme()->palette().text().color();
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (index.column() == 0)
|
||||
return m_categories.at(index.row())->name;
|
||||
if (index.column() == 1) {
|
||||
return LoggingViewManager::messageTypeToString(
|
||||
m_categories.at(index.row())->entry.level);
|
||||
}
|
||||
}
|
||||
if (role == Qt::DecorationRole && index.column() == 2) {
|
||||
const QColor color = m_categories.at(index.row())->entry.color;
|
||||
if (color.isValid())
|
||||
return color;
|
||||
return defaultColor;
|
||||
}
|
||||
if (role == Qt::CheckStateRole && index.column() == 0) {
|
||||
const LoggingCategoryEntry entry = m_categories.at(index.row())->entry;
|
||||
return entry.enabled ? Qt::Checked : Qt::Unchecked;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool LoggingCategoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return false;
|
||||
|
||||
if (role == Qt::CheckStateRole && index.column() == 0) {
|
||||
LoggingCategoryItem *item = m_categories.at(index.row());
|
||||
const Qt::CheckState current = item->entry.enabled ? Qt::Checked : Qt::Unchecked;
|
||||
if (current != value.toInt()) {
|
||||
item->entry.enabled = !item->entry.enabled;
|
||||
emit categoryChanged(item->name, item->entry.enabled);
|
||||
return true;
|
||||
}
|
||||
} else if (role == Qt::DecorationRole && index.column() == 2) {
|
||||
LoggingCategoryItem *item = m_categories.at(index.row());
|
||||
QColor color = value.value<QColor>();
|
||||
if (color.isValid() && color != item->entry.color) {
|
||||
item->entry.color = color;
|
||||
emit colorChanged(item->name, color);
|
||||
return true;
|
||||
}
|
||||
} else if (role == Qt::EditRole && index.column() == 1) {
|
||||
LoggingCategoryItem *item = m_categories.at(index.row());
|
||||
item->entry.level = LoggingViewManager::messageTypeFromString(value.toString());
|
||||
emit logLevelChanged(item->name, item->entry.level);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::ItemFlags LoggingCategoryModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
// ItemIsEnabled should depend on availability (Qt logging enabled?)
|
||||
if (index.column() == 0)
|
||||
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
if (index.column() == 1)
|
||||
return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QVariant LoggingCategoryModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal && section >= 0 && section < 3) {
|
||||
switch (section) {
|
||||
case 0: return tr("Category");
|
||||
case 1: return tr("Type");
|
||||
case 2: return tr("Color");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void LoggingCategoryModel::reset()
|
||||
{
|
||||
beginResetModel();
|
||||
qDeleteAll(m_categories);
|
||||
m_categories.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void LoggingCategoryModel::setFromManager(LoggingViewManager *manager)
|
||||
{
|
||||
beginResetModel();
|
||||
qDeleteAll(m_categories);
|
||||
m_categories.clear();
|
||||
const QMap<QString, LoggingCategoryEntry> categories = manager->categories();
|
||||
auto it = categories.begin();
|
||||
for (auto end = categories.end() ; it != end; ++it)
|
||||
m_categories.append(new LoggingCategoryItem{it.key(), it.value()});
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QList<LoggingCategoryItem> LoggingCategoryModel::enabledCategories() const
|
||||
{
|
||||
QList<LoggingCategoryItem> result;
|
||||
for (auto item : m_categories) {
|
||||
if (item->entry.enabled)
|
||||
result.append({item->name, item->entry});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void LoggingCategoryModel::disableAll()
|
||||
{
|
||||
for (int row = 0, end = m_categories.count(); row < end; ++row)
|
||||
setData(index(row, 0), Qt::Unchecked, Qt::CheckStateRole);
|
||||
}
|
||||
|
||||
class LoggingLevelDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
explicit LoggingLevelDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}
|
||||
~LoggingLevelDelegate() = default;
|
||||
|
||||
protected:
|
||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
|
||||
void setModelData(QWidget *editor, QAbstractItemModel *model,
|
||||
const QModelIndex &index) const override;
|
||||
};
|
||||
|
||||
QWidget *LoggingLevelDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/,
|
||||
const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid() || index.column() != 1)
|
||||
return nullptr;
|
||||
QComboBox *combo = new QComboBox(parent);
|
||||
combo->addItems({ {"Critical"}, {"Warning"}, {"Debug"}, {"Info"} });
|
||||
return combo;
|
||||
}
|
||||
|
||||
void LoggingLevelDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
|
||||
{
|
||||
QComboBox *combo = qobject_cast<QComboBox *>(editor);
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
const int i = combo->findText(index.data().toString());
|
||||
if (i >= 0)
|
||||
combo->setCurrentIndex(i);
|
||||
}
|
||||
|
||||
void LoggingLevelDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
|
||||
const QModelIndex &index) const
|
||||
{
|
||||
QComboBox *combo = qobject_cast<QComboBox *>(editor);
|
||||
if (combo)
|
||||
model->setData(index, combo->currentText());
|
||||
}
|
||||
|
||||
class LogEntry
|
||||
{
|
||||
public:
|
||||
QString timestamp;
|
||||
QString category;
|
||||
QString type;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
class LoggingViewManagerWidget : public QDialog
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(LoggingViewManagerWidget)
|
||||
public:
|
||||
explicit LoggingViewManagerWidget(QWidget *parent);
|
||||
~LoggingViewManagerWidget()
|
||||
{
|
||||
setEnabled(false);
|
||||
delete m_manager;
|
||||
}
|
||||
|
||||
static QColor colorForCategory(const QString &category);
|
||||
private:
|
||||
void showLogViewContextMenu(const QPoint &pos) const;
|
||||
void showLogCategoryContextMenu(const QPoint &pos) const;
|
||||
void saveLoggingsToFile() const;
|
||||
void saveEnabledCategoryPreset() const;
|
||||
void loadAndUpdateFromPreset();
|
||||
LoggingViewManager *m_manager = nullptr;
|
||||
void setCategoryColor(const QString &category, const QColor &color);
|
||||
// should category model be owned directly by the manager? or is this duplication of
|
||||
// categories in manager and widget beneficial?
|
||||
LoggingCategoryModel *m_categoryModel = nullptr;
|
||||
Utils::BaseTreeView *m_logView = nullptr;
|
||||
Utils::BaseTreeView *m_categoryView = nullptr;
|
||||
Utils::ListModel<LogEntry> *m_logModel = nullptr;
|
||||
QToolButton *m_timestamps = nullptr;
|
||||
QToolButton *m_messageTypes = nullptr;
|
||||
static QHash<QString, QColor> m_categoryColor;
|
||||
};
|
||||
|
||||
QHash<QString, QColor> LoggingViewManagerWidget::m_categoryColor;
|
||||
|
||||
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: return entry.message;
|
||||
}
|
||||
}
|
||||
if (role == Qt::TextAlignmentRole)
|
||||
return Qt::AlignTop;
|
||||
if (column == 1 && role == Qt::ForegroundRole)
|
||||
return LoggingViewManagerWidget::colorForCategory(entry.category);
|
||||
return {};
|
||||
}
|
||||
|
||||
LoggingViewManagerWidget::LoggingViewManagerWidget(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_manager(new LoggingViewManager)
|
||||
{
|
||||
setWindowTitle(tr("Logging Category Viewer"));
|
||||
setModal(false);
|
||||
|
||||
auto mainLayout = new QVBoxLayout;
|
||||
|
||||
auto buttonsLayout = new QHBoxLayout;
|
||||
buttonsLayout->setSpacing(0);
|
||||
// add further buttons..
|
||||
auto save = new QToolButton;
|
||||
save->setIcon(Utils::Icons::SAVEFILE.icon());
|
||||
save->setToolTip(tr("Save Log"));
|
||||
buttonsLayout->addWidget(save);
|
||||
auto clean = new QToolButton;
|
||||
clean->setIcon(Utils::Icons::CLEAN.icon());
|
||||
clean->setToolTip(tr("Clear"));
|
||||
buttonsLayout->addWidget(clean);
|
||||
auto stop = new QToolButton;
|
||||
stop->setIcon(Utils::Icons::STOP_SMALL.icon());
|
||||
stop->setToolTip(tr("Stop Logging"));
|
||||
buttonsLayout->addWidget(stop);
|
||||
auto qtInternal = new QToolButton;
|
||||
qtInternal->setIcon(Core::Icons::QTLOGO.icon());
|
||||
qtInternal->setToolTip(tr("Toggle logging of Qt internal loggings"));
|
||||
qtInternal->setCheckable(true);
|
||||
qtInternal->setChecked(false);
|
||||
buttonsLayout->addWidget(qtInternal);
|
||||
auto autoScroll = new QToolButton;
|
||||
autoScroll->setIcon(Utils::Icons::ARROW_DOWN.icon());
|
||||
autoScroll->setToolTip(tr("Auto Scroll"));
|
||||
autoScroll->setCheckable(true);
|
||||
autoScroll->setChecked(true);
|
||||
buttonsLayout->addWidget(autoScroll);
|
||||
m_timestamps = new QToolButton;
|
||||
auto icon = Utils::Icon({{":/utils/images/stopwatch.png", Utils::Theme::PanelTextColorMid}},
|
||||
Utils::Icon::Tint);
|
||||
m_timestamps->setIcon(icon.icon());
|
||||
m_timestamps->setToolTip(tr("Timestamps"));
|
||||
m_timestamps->setCheckable(true);
|
||||
m_timestamps->setChecked(true);
|
||||
buttonsLayout->addWidget(m_timestamps);
|
||||
m_messageTypes = new QToolButton;
|
||||
icon = Utils::Icon({{":/utils/images/message.png", Utils::Theme::PanelTextColorMid}},
|
||||
Utils::Icon::Tint);
|
||||
m_messageTypes->setIcon(icon.icon());
|
||||
m_messageTypes->setToolTip(tr("Message Types"));
|
||||
m_messageTypes->setCheckable(true);
|
||||
m_messageTypes->setChecked(false);
|
||||
buttonsLayout->addWidget(m_messageTypes);
|
||||
|
||||
buttonsLayout->addSpacerItem(new QSpacerItem(10, 10, QSizePolicy::Expanding));
|
||||
mainLayout->addLayout(buttonsLayout);
|
||||
|
||||
auto horizontal = new QHBoxLayout;
|
||||
m_logView = new Utils::BaseTreeView;
|
||||
m_logModel = new Utils::ListModel<LogEntry>;
|
||||
m_logModel->setHeader({tr("Timestamp"), tr("Category"), tr("Type"), tr("Message")});
|
||||
m_logModel->setDataAccessor(&logEntryDataAccessor);
|
||||
m_logView->setModel(m_logModel);
|
||||
horizontal->addWidget(m_logView);
|
||||
m_logView->setUniformRowHeights(false);
|
||||
m_logView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_logView->setFrameStyle(QFrame::Box);
|
||||
m_logView->setTextElideMode(Qt::ElideNone);
|
||||
m_logView->setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
m_logView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_logView->setColumnHidden(2, true);
|
||||
m_logView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
m_categoryView = new Utils::BaseTreeView;
|
||||
m_categoryView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_categoryView->setUniformRowHeights(true);
|
||||
m_categoryView->setFrameStyle(QFrame::Box);
|
||||
m_categoryView->setTextElideMode(Qt::ElideNone);
|
||||
m_categoryView->setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
m_categoryView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_categoryView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_categoryModel = new LoggingCategoryModel;
|
||||
m_categoryModel->setFromManager(m_manager);
|
||||
auto sortFilterModel = new QSortFilterProxyModel(this);
|
||||
sortFilterModel->setSourceModel(m_categoryModel);
|
||||
sortFilterModel->sort(0);
|
||||
m_categoryView->setModel(sortFilterModel);
|
||||
m_categoryView->setItemDelegateForColumn(1, new LoggingLevelDelegate(this));
|
||||
horizontal->addWidget(m_categoryView);
|
||||
horizontal->setStretch(0, 5);
|
||||
horizontal->setStretch(1, 3);
|
||||
|
||||
mainLayout->addLayout(horizontal);
|
||||
setLayout(mainLayout);
|
||||
resize(800, 300);
|
||||
|
||||
connect(m_manager, &LoggingViewManager::receivedLog,
|
||||
this, [this, autoScroll](const QString ×tamp, const QString &type,
|
||||
const QString &category, const QString &msg) {
|
||||
if (m_logModel->rowCount() >= 1000000) // limit log to 1000000 items
|
||||
m_logModel->destroyItem(m_logModel->itemForIndex(m_logModel->index(0, 0)));
|
||||
m_logModel->appendItem(LogEntry{timestamp, type, category, msg});
|
||||
if (autoScroll->isChecked())
|
||||
m_logView->scrollToBottom();
|
||||
}, Qt::QueuedConnection);
|
||||
connect(m_manager, &LoggingViewManager::foundNewCategory,
|
||||
m_categoryModel, &LoggingCategoryModel::append, Qt::QueuedConnection);
|
||||
connect(m_manager, &LoggingViewManager::updatedCategory,
|
||||
m_categoryModel, &LoggingCategoryModel::update, Qt::QueuedConnection);
|
||||
connect(m_categoryModel, &LoggingCategoryModel::categoryChanged,
|
||||
m_manager, &LoggingViewManager::setCategoryEnabled);
|
||||
connect(m_categoryModel, &LoggingCategoryModel::colorChanged,
|
||||
this, &LoggingViewManagerWidget::setCategoryColor);
|
||||
connect(m_categoryModel, &LoggingCategoryModel::logLevelChanged,
|
||||
m_manager, &LoggingViewManager::setLogLevel);
|
||||
connect(m_categoryView, &Utils::BaseTreeView::activated,
|
||||
this, [this, sortFilterModel](const QModelIndex &index) {
|
||||
const QModelIndex modelIndex = sortFilterModel->mapToSource(index);
|
||||
const QVariant value = m_categoryModel->data(modelIndex, 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_categoryModel->setData(modelIndex, changed, Qt::DecorationRole);
|
||||
});
|
||||
connect(save, &QToolButton::clicked,
|
||||
this, &LoggingViewManagerWidget::saveLoggingsToFile);
|
||||
connect(m_logView, &Utils::BaseTreeView::customContextMenuRequested,
|
||||
this, &LoggingViewManagerWidget::showLogViewContextMenu);
|
||||
connect(m_categoryView, &Utils::BaseTreeView::customContextMenuRequested,
|
||||
this, &LoggingViewManagerWidget::showLogCategoryContextMenu);
|
||||
connect(clean, &QToolButton::clicked, m_logModel, &Utils::ListModel<LogEntry>::clear);
|
||||
connect(stop, &QToolButton::clicked, this, [this, stop]() {
|
||||
if (m_manager->isEnabled()) {
|
||||
m_manager->setEnabled(false);
|
||||
stop->setIcon(Utils::Icons::RUN_SMALL.icon());
|
||||
stop->setToolTip(tr("Start Logging"));
|
||||
} else {
|
||||
m_manager->setEnabled(true);
|
||||
stop->setIcon(Utils::Icons::STOP_SMALL.icon());
|
||||
stop->setToolTip(tr("Stop Logging"));
|
||||
}
|
||||
});
|
||||
connect(qtInternal, &QToolButton::toggled, m_manager, &LoggingViewManager::setListQtInternal);
|
||||
connect(m_timestamps, &QToolButton::toggled, this, [this](bool checked){
|
||||
m_logView->setColumnHidden(0, !checked);
|
||||
});
|
||||
connect(m_messageTypes, &QToolButton::toggled, this, [this](bool checked){
|
||||
m_logView->setColumnHidden(2, !checked);
|
||||
});
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::showLogViewContextMenu(const QPoint &pos) const
|
||||
{
|
||||
QMenu m;
|
||||
auto copy = new QAction(tr("Copy Selected Logs"), &m);
|
||||
m.addAction(copy);
|
||||
auto copyAll = new QAction(tr("Copy All"), &m);
|
||||
m.addAction(copyAll);
|
||||
connect(copy, &QAction::triggered, &m, [this](){
|
||||
auto selectionModel = m_logView->selectionModel();
|
||||
QString copied;
|
||||
const bool useTS = m_timestamps->isChecked();
|
||||
const bool useLL = m_messageTypes->isChecked();
|
||||
for (int row = 0, end = m_logModel->rowCount(); row < end; ++row) {
|
||||
if (selectionModel->isRowSelected(row, QModelIndex()))
|
||||
copied.append(m_logModel->dataAt(row).outputLine(useTS, useLL));
|
||||
}
|
||||
|
||||
QGuiApplication::clipboard()->setText(copied);
|
||||
});
|
||||
connect(copyAll, &QAction::triggered, &m, [this](){
|
||||
QString copied;
|
||||
const bool useTS = m_timestamps->isChecked();
|
||||
const bool useLL = m_messageTypes->isChecked();
|
||||
|
||||
for (int row = 0, end = m_logModel->rowCount(); row < end; ++row)
|
||||
copied.append(m_logModel->dataAt(row).outputLine(useTS, useLL));
|
||||
|
||||
QGuiApplication::clipboard()->setText(copied);
|
||||
});
|
||||
m.exec(m_logView->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::showLogCategoryContextMenu(const QPoint &pos) const
|
||||
{
|
||||
QMenu m;
|
||||
// minimal load/save - plugins could later provide presets on their own?
|
||||
auto savePreset = new QAction(tr("Save Enabled as Preset..."), &m);
|
||||
m.addAction(savePreset);
|
||||
auto loadPreset = new QAction(tr("Update from Preset..."), &m);
|
||||
m.addAction(loadPreset);
|
||||
auto uncheckAll = new QAction(tr("Uncheck All"), &m);
|
||||
m.addAction(uncheckAll);
|
||||
connect(savePreset, &QAction::triggered,
|
||||
this, &LoggingViewManagerWidget::saveEnabledCategoryPreset);
|
||||
connect(loadPreset, &QAction::triggered,
|
||||
this, &LoggingViewManagerWidget::loadAndUpdateFromPreset);
|
||||
connect(uncheckAll, &QAction::triggered,
|
||||
m_categoryModel, &LoggingCategoryModel::disableAll);
|
||||
m.exec(m_categoryView->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::saveLoggingsToFile() const
|
||||
{
|
||||
// should we just let it continue without temporarily disabling?
|
||||
const bool enabled = m_manager->isEnabled();
|
||||
Utils::ExecuteOnDestruction exec([this, enabled]() { m_manager->setEnabled(enabled); });
|
||||
if (enabled)
|
||||
m_manager->setEnabled(false);
|
||||
const Utils::FilePath fp = Utils::FileUtils::getSaveFilePath(ICore::dialogParent(),
|
||||
tr("Save logs as"));
|
||||
if (fp.isEmpty())
|
||||
return;
|
||||
const bool useTS = m_timestamps->isChecked();
|
||||
const bool useLL = m_messageTypes->isChecked();
|
||||
QFile file(fp.path());
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
for (int row = 0, end = m_logModel->rowCount(); row < end; ++row) {
|
||||
qint64 res = file.write( m_logModel->dataAt(row).outputLine(useTS, useLL).toUtf8());
|
||||
if (res == -1) {
|
||||
QMessageBox::critical(
|
||||
ICore::dialogParent(), tr("Error"),
|
||||
tr("Failed to write logs to '%1'.").arg(fp.toUserOutput()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
ICore::dialogParent(), tr("Error"),
|
||||
tr("Failed to open file '%1' for writing logs.").arg(fp.toUserOutput()));
|
||||
}
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::saveEnabledCategoryPreset() const
|
||||
{
|
||||
Utils::FilePath fp = Utils::FileUtils::getSaveFilePath(ICore::dialogParent(),
|
||||
tr("Save enabled categories as"));
|
||||
if (fp.isEmpty())
|
||||
return;
|
||||
const QList<LoggingCategoryItem> enabled = m_categoryModel->enabledCategories();
|
||||
// write them to file
|
||||
QJsonArray array;
|
||||
for (const LoggingCategoryItem &item : enabled) {
|
||||
QJsonObject itemObj;
|
||||
itemObj.insert("name", item.name);
|
||||
QJsonObject entryObj;
|
||||
entryObj.insert("level", item.entry.level);
|
||||
if (item.entry.color.isValid())
|
||||
entryObj.insert("color", item.entry.color.name(QColor::HexArgb));
|
||||
itemObj.insert("entry", entryObj);
|
||||
array.append(itemObj);
|
||||
}
|
||||
QJsonDocument doc(array);
|
||||
if (!fp.writeFileContents(doc.toJson(QJsonDocument::Compact)))
|
||||
QMessageBox::critical(
|
||||
ICore::dialogParent(), tr("Error"),
|
||||
tr("Failed to write preset file '%1'.").arg(fp.toUserOutput()));
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::loadAndUpdateFromPreset()
|
||||
{
|
||||
Utils::FilePath fp = Utils::FileUtils::getOpenFilePath(ICore::dialogParent(),
|
||||
tr("Load enabled categories from"));
|
||||
if (fp.isEmpty())
|
||||
return;
|
||||
// read file, update categories
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(fp.fileContents(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
QMessageBox::critical(ICore::dialogParent(), tr("Error"),
|
||||
tr("Failed to read preset file '%1': %2").arg(fp.toUserOutput())
|
||||
.arg(error.errorString()));
|
||||
return;
|
||||
}
|
||||
bool formatError = false;
|
||||
QList<LoggingCategoryItem> presetItems;
|
||||
if (doc.isArray()) {
|
||||
const QJsonArray array = doc.array();
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject()) {
|
||||
formatError = true;
|
||||
break;
|
||||
}
|
||||
const QJsonObject itemObj = value.toObject();
|
||||
bool ok = true;
|
||||
LoggingCategoryItem item = LoggingCategoryItem::fromJson(itemObj, &ok);
|
||||
if (!ok) {
|
||||
formatError = true;
|
||||
break;
|
||||
}
|
||||
presetItems.append(item);
|
||||
}
|
||||
} else {
|
||||
formatError = true;
|
||||
}
|
||||
|
||||
if (formatError) {
|
||||
QMessageBox::critical(ICore::dialogParent(), tr("Error"),
|
||||
tr("Unexpected preset file format."));
|
||||
}
|
||||
for (const LoggingCategoryItem &item : presetItems)
|
||||
m_manager->appendOrUpdate(item.name, item.entry);
|
||||
}
|
||||
|
||||
QColor LoggingViewManagerWidget::colorForCategory(const QString &category)
|
||||
{
|
||||
auto entry = m_categoryColor.find(category);
|
||||
if (entry == m_categoryColor.end())
|
||||
return Utils::creatorTheme()->palette().text().color();
|
||||
return entry.value();
|
||||
}
|
||||
|
||||
void LoggingViewManagerWidget::setCategoryColor(const QString &category, const QColor &color)
|
||||
{
|
||||
const QColor baseColor = Utils::creatorTheme()->palette().text().color();
|
||||
if (color != baseColor)
|
||||
m_categoryColor.insert(category, color);
|
||||
else
|
||||
m_categoryColor.remove(category);
|
||||
}
|
||||
|
||||
void LoggingViewer::showLoggingView()
|
||||
{
|
||||
ActionManager::command(Constants::LOGGER)->action()->setEnabled(false);
|
||||
auto widget = new LoggingViewManagerWidget(ICore::mainWindow());
|
||||
QObject::connect(widget, &QDialog::finished, widget, [widget] () {
|
||||
ActionManager::command(Constants::LOGGER)->action()->setEnabled(true);
|
||||
// explicitly disable manager again
|
||||
widget->deleteLater();
|
||||
});
|
||||
widget->show();
|
||||
}
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Core
|
||||
|
||||
#include "loggingviewer.moc"
|
38
src/plugins/coreplugin/loggingviewer.h
Normal file
38
src/plugins/coreplugin/loggingviewer.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Core {
|
||||
namespace Internal {
|
||||
|
||||
class LoggingViewer
|
||||
{
|
||||
public:
|
||||
static void showLoggingView();
|
||||
};
|
||||
|
||||
} // Internal
|
||||
} // Core
|
@@ -32,6 +32,7 @@
|
||||
#include "documentmanager.h"
|
||||
#include "generalsettings.h"
|
||||
#include "idocumentfactory.h"
|
||||
#include "loggingviewer.h"
|
||||
#include "messagemanager.h"
|
||||
#include "modemanager.h"
|
||||
#include "outputpanemanager.h"
|
||||
@@ -704,6 +705,12 @@ void MainWindow::registerDefaultActions()
|
||||
mtools->appendGroup(Constants::G_TOOLS_OPTIONS);
|
||||
mtools->addSeparator(Constants::G_TOOLS_OPTIONS);
|
||||
|
||||
m_loggerAction = new QAction(tr("Logger..."), this);
|
||||
cmd = ActionManager::registerAction(m_loggerAction, Constants::LOGGER);
|
||||
mtools->addAction(cmd, Constants::G_TOOLS_OPTIONS);
|
||||
connect(m_loggerAction, &QAction::triggered, this, [] { LoggingViewer::showLoggingView(); });
|
||||
mtools->addSeparator(Constants::G_TOOLS_OPTIONS);
|
||||
|
||||
m_optionsAction = new QAction(tr("&Options..."), this);
|
||||
m_optionsAction->setMenuRole(QAction::PreferencesRole);
|
||||
cmd = ActionManager::registerAction(m_optionsAction, Constants::OPTIONS);
|
||||
|
@@ -188,6 +188,7 @@ private:
|
||||
QAction *m_saveAllAction = nullptr;
|
||||
QAction *m_exitAction = nullptr;
|
||||
QAction *m_optionsAction = nullptr;
|
||||
QAction *m_loggerAction = nullptr;
|
||||
QAction *m_toggleLeftSideBarAction = nullptr;
|
||||
QAction *m_toggleRightSideBarAction = nullptr;
|
||||
QAction *m_cycleModeSelectorStyleAction = nullptr;
|
||||
|
Reference in New Issue
Block a user