From 5eafa345edcd9fa91ab81e2289611d99422c60b8 Mon Sep 17 00:00:00 2001 From: Christian Stenger Date: Thu, 19 Aug 2021 22:32:21 +0200 Subject: [PATCH] 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 Reviewed-by: Alessandro Portale --- src/libs/utils/images/message.png | Bin 0 -> 379 bytes src/libs/utils/images/message@2x.png | Bin 0 -> 745 bytes src/libs/utils/utils.qrc | 2 + src/plugins/coreplugin/CMakeLists.txt | 2 + src/plugins/coreplugin/core.qrc | 14 +- src/plugins/coreplugin/coreconstants.h | 1 + src/plugins/coreplugin/coreicons.cpp | 2 + src/plugins/coreplugin/coreicons.h | 1 + src/plugins/coreplugin/coreplugin.pro | 4 + src/plugins/coreplugin/coreplugin.qbs | 4 + .../images/logo/16/Qt_logo_green.png | Bin 0 -> 404 bytes .../images/logo/32/Qt_logo_green.png | Bin 0 -> 681 bytes src/plugins/coreplugin/loggingmanager.cpp | 323 ++++++++ src/plugins/coreplugin/loggingmanager.h | 143 ++++ src/plugins/coreplugin/loggingviewer.cpp | 740 ++++++++++++++++++ src/plugins/coreplugin/loggingviewer.h | 38 + src/plugins/coreplugin/mainwindow.cpp | 7 + src/plugins/coreplugin/mainwindow.h | 1 + 18 files changed, 1276 insertions(+), 6 deletions(-) create mode 100644 src/libs/utils/images/message.png create mode 100644 src/libs/utils/images/message@2x.png create mode 100644 src/plugins/coreplugin/images/logo/16/Qt_logo_green.png create mode 100644 src/plugins/coreplugin/images/logo/32/Qt_logo_green.png create mode 100644 src/plugins/coreplugin/loggingmanager.cpp create mode 100644 src/plugins/coreplugin/loggingmanager.h create mode 100644 src/plugins/coreplugin/loggingviewer.cpp create mode 100644 src/plugins/coreplugin/loggingviewer.h diff --git a/src/libs/utils/images/message.png b/src/libs/utils/images/message.png new file mode 100644 index 0000000000000000000000000000000000000000..b5540fa0e6569c4d41d8c14a7b9801c64a8db90a GIT binary patch literal 379 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7Sc;uILpV4%IBGajIv5xj zI14-?iy0XBj({-ZRBb+K1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xh zq!<_&et5b#hFJ6-oov|EVj$A?UuQ8#*|a#v-&^7ioHN#lh~yNTr#M&q17BIw1Z4@9 zw|koe0yj@xbmrX|gUuV_zY5*^Z~xETX-W{6{D-fdKm2*}syF6JcP(PbciD-) z*EwC3|3R<4aapE*cS-DibB@CTsgX`zKUW8Mb#BrTbiG*UeojA-gYlTet6gsMo@d+^ zdu(xNlMX{e?rn~y2b&MsT~s%1esH<`-WjY3^7V-MZ)kUsQnm!GlLn zuB~@CxVEWVQ){DaRK{HGl7cY9CU17f4bBIutbS(1Z?Vgiv#Y2pSYBMT|Gw?c=W@b= zg4ZO5Q%$v99W{%SAvrZyMr~3Wd z9Ty)z@%;0LU#kLj#HKUb*xD*e@H~3;imOTC&xx944+}a{j5=@Mj{Z@zPv7va{)ALX zcX#);namAm)0)pecitIu?Pv+Zs#Qt(`R5Hj&wNv6UA6aKQNzg;rfI3X>7)z{VTbvZ%ynny`&aC(E-v@+>UdnDDaB}J-P4*loAbdYQcLr; z?>^D?jBoMzOD4Q8OQc$z7-#yh3vtq;Hdo_JccA!=>B_wKE&ckjj) z$b7F|DYi&3aR2?~j~^!sxlYv6*IyZ+5nv&sHt+e##~&~J{rk6k{{M+$Od(pLw{G54 z+a&mHYIeSD7JS>=SHch$vsM92sK)dimages/online@2x.png images/download.png images/download@2x.png + images/message.png + images/message@2x.png ../3rdparty/xdg/freedesktop.org.xml diff --git a/src/plugins/coreplugin/CMakeLists.txt b/src/plugins/coreplugin/CMakeLists.txt index 8acbfdd8a2e..ce13bb76b7d 100644 --- a/src/plugins/coreplugin/CMakeLists.txt +++ b/src/plugins/coreplugin/CMakeLists.txt @@ -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 diff --git a/src/plugins/coreplugin/core.qrc b/src/plugins/coreplugin/core.qrc index 5bd3d568832..3804d793098 100644 --- a/src/plugins/coreplugin/core.qrc +++ b/src/plugins/coreplugin/core.qrc @@ -1,10 +1,12 @@ - images/logo/128/QtProject-qtcreator.png - images/logo/256/QtProject-qtcreator.png - images/settingscategory_core.png - images/settingscategory_core@2x.png - images/settingscategory_design.png - images/settingscategory_design@2x.png + images/logo/128/QtProject-qtcreator.png + images/logo/256/QtProject-qtcreator.png + images/settingscategory_core.png + images/settingscategory_core@2x.png + images/settingscategory_design.png + images/settingscategory_design@2x.png + images/logo/16/Qt_logo_green.png + images/logo/32/Qt_logo_green.png diff --git a/src/plugins/coreplugin/coreconstants.h b/src/plugins/coreplugin/coreconstants.h index 29e420dfcdd..ef0302293e9 100644 --- a/src/plugins/coreplugin/coreconstants.h +++ b/src/plugins/coreplugin/coreconstants.h @@ -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[] = diff --git a/src/plugins/coreplugin/coreicons.cpp b/src/plugins/coreplugin/coreicons.cpp index fd3901f2971..e91b813ab41 100644 --- a/src/plugins/coreplugin/coreicons.cpp +++ b/src/plugins/coreplugin/coreicons.cpp @@ -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( diff --git a/src/plugins/coreplugin/coreicons.h b/src/plugins/coreplugin/coreicons.h index dd0292ba9a5..6c26b9f066b 100644 --- a/src/plugins/coreplugin/coreicons.h +++ b/src/plugins/coreplugin/coreicons.h @@ -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; diff --git a/src/plugins/coreplugin/coreplugin.pro b/src/plugins/coreplugin/coreplugin.pro index 3a08ec5dbda..7f6902440bb 100644 --- a/src/plugins/coreplugin/coreplugin.pro +++ b/src/plugins/coreplugin/coreplugin.pro @@ -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 \ diff --git a/src/plugins/coreplugin/coreplugin.qbs b/src/plugins/coreplugin/coreplugin.qbs index 51e82ee2c1e..3af3e92f9d1 100644 --- a/src/plugins/coreplugin/coreplugin.qbs +++ b/src/plugins/coreplugin/coreplugin.qbs @@ -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", diff --git a/src/plugins/coreplugin/images/logo/16/Qt_logo_green.png b/src/plugins/coreplugin/images/logo/16/Qt_logo_green.png new file mode 100644 index 0000000000000000000000000000000000000000..e4623bb7fb39d89126c8f44a80299edc253c39f4 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4rT@h1`S>QU(BJt_%ze|NsAYJR5|J zoX!Tjo(uChAL)5M()&Vm=%tj{D>?C3a}%!SrCl${zEN3lv$p6~eaY>{^4ra|cRQNz z^|jpZZ@oXE>%o+shtnrLnlt6`yy;IC%zCsPl&6fU$t*ybE-OU|D-(}h(I$f=;;a7Ei#nc;Y z#tsrc-#wYB9Ifr-^hd=!bYD!g%g(-(qbIt4GDLB3|JbDRB87p0fx*+&&t;ucLK6T+ CO3{A+ literal 0 HcmV?d00001 diff --git a/src/plugins/coreplugin/images/logo/32/Qt_logo_green.png b/src/plugins/coreplugin/images/logo/32/Qt_logo_green.png new file mode 100644 index 0000000000000000000000000000000000000000..1f2de695c08365192ff06bd5347e873a26208fcd GIT binary patch literal 681 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGO#^&FTp1V`{{R2)cs2+b zIh_r5J{RJ0F4Pr-!n`j;`(B9kyAbDhF)r|8V(6um*els_S921s<|SV%Ou1H+daXF) zdTGY>vdkOhg}3U;ZZ}okX{))@(QvoB<$izL{fV6qCU-rU(*0m+-@}=c9?hBjc$l4QpO*S^IL! zx>s8_z233q^{y?ickg(!fA`yid*2=2_wLC4cSjGrKX&l_@x$*=9{F(c_=htmKAb)I z@$Bi37tVgVbpF%j3!krC{(SxFmmAl=+`94g&dslPZhgIX``f)c-|j#7{`ledCy#$T zef;Cun_q7~{{Hmo_vcT4K7ao6<;$P1U;ljj_V@euzdwHc`}O64!rI0kMIcX?AQ8u;x0e-?9Txjo(`_h(F>nX+&46BYGEC*2n@HY`q<@o!qZf1PdT zf&aFh0lF^B`@N?BPCi@eaCZ0n2d~tg@$hYoV;A{2(<>_az&1G#hR2u813IOi85%9S zd8&bRO7xO)#<2Cba;7j^?+AFvY+U$TTJ*MD+5ydPAr&rB)oo0hwC zltO>8Xq>@%*02vU6Ynwd2+1luW@T$oeCfSnn{evH?@Y&JPo4J6-rN;izW4i=Ec^Yh z_Jx<)Ue7(J@bBpoPriVhZ%aaIoDbT1oqu#IWI20*DPzxv>wJX@MXl_Qzq?o*VDvgO pa}7Ti{{hwNhu6&?z5dB)Z@tQCRov|5sSFGZ44$rjF6*2UngC`1e3<|M literal 0 HcmV?d00001 diff --git a/src/plugins/coreplugin/loggingmanager.cpp b/src/plugins/coreplugin/loggingmanager.cpp new file mode 100644 index 00000000000..d4eb2e14286 --- /dev/null +++ b/src/plugins/coreplugin/loggingmanager.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +// +// 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 fetchOriginalRules() +{ + QList 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(); + 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 diff --git a/src/plugins/coreplugin/loggingmanager.h b/src/plugins/coreplugin/loggingmanager.h new file mode 100644 index 00000000000..07c7192f539 --- /dev/null +++ b/src/plugins/coreplugin/loggingmanager.h @@ -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 + +#include +#include +#include +#include + +namespace Core { +namespace Internal { + +struct FilterRuleSpec +{ + QString category; + Utils::optional 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 originalRules() const { return m_originalRules; } + + QMap 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 m_categories; + const QString m_originalLoggingRules; + QList m_originalRules; + bool m_enabled = false; + bool m_listQtInternal = false; +}; + +} // namespace Internal +} // namespace Core + +Q_DECLARE_METATYPE(Core::Internal::LoggingCategoryEntry) diff --git a/src/plugins/coreplugin/loggingviewer.cpp b/src/plugins/coreplugin/loggingviewer.cpp new file mode 100644 index 00000000000..7120943d6aa --- /dev/null +++ b/src/plugins/coreplugin/loggingviewer.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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(); + 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 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 LoggingCategoryModel::enabledCategories() const +{ + QList 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(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(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 *m_logModel = nullptr; + QToolButton *m_timestamps = nullptr; + QToolButton *m_messageTypes = nullptr; + static QHash m_categoryColor; +}; + +QHash 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; + 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(); + 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::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 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 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" diff --git a/src/plugins/coreplugin/loggingviewer.h b/src/plugins/coreplugin/loggingviewer.h new file mode 100644 index 00000000000..377a0505306 --- /dev/null +++ b/src/plugins/coreplugin/loggingviewer.h @@ -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 diff --git a/src/plugins/coreplugin/mainwindow.cpp b/src/plugins/coreplugin/mainwindow.cpp index 5039fd0ceca..f23c0040d3f 100644 --- a/src/plugins/coreplugin/mainwindow.cpp +++ b/src/plugins/coreplugin/mainwindow.cpp @@ -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); diff --git a/src/plugins/coreplugin/mainwindow.h b/src/plugins/coreplugin/mainwindow.h index 4631986c584..b365c0c1ee0 100644 --- a/src/plugins/coreplugin/mainwindow.h +++ b/src/plugins/coreplugin/mainwindow.h @@ -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;