summaryrefslogtreecommitdiff
path: root/src/plugins/coreplugin/loggingviewer.cpp
diff options
context:
space:
mode:
authorChristian Stenger <christian.stenger@qt.io>2021-08-19 22:32:21 +0200
committerChristian Stenger <christian.stenger@qt.io>2022-01-17 06:08:58 +0000
commit5eafa345edcd9fa91ab81e2289611d99422c60b8 (patch)
tree09b5f7b0d99680c33bdbfe2e1f06eee0828b5ecf /src/plugins/coreplugin/loggingviewer.cpp
parent9c7b1d39d392d737b68144fd21c0fd991b89cd39 (diff)
downloadqt-creator-5eafa345edcd9fa91ab81e2289611d99422c60b8.tar.gz
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>
Diffstat (limited to 'src/plugins/coreplugin/loggingviewer.cpp')
-rw-r--r--src/plugins/coreplugin/loggingviewer.cpp740
1 files changed, 740 insertions, 0 deletions
diff --git a/src/plugins/coreplugin/loggingviewer.cpp b/src/plugins/coreplugin/loggingviewer.cpp
new file mode 100644
index 0000000000..7120943d6a
--- /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 <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 &timestamp, 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"