/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "loglist.h" #include "copyeventfilter.h" #include "env.h" #include "organizercore.h" using namespace MOBase; static LogModel* g_instance = nullptr; const std::size_t MaxLines = 1000; static std::unique_ptr m_console; static bool m_stdout = false; static std::mutex m_stdoutMutex; LogModel::LogModel() {} void LogModel::create() { g_instance = new LogModel; } LogModel& LogModel::instance() { return *g_instance; } void LogModel::add(MOBase::log::Entry e) { QMetaObject::invokeMethod( this, [this, e] { onEntryAdded(std::move(e)); }, Qt::QueuedConnection); } QString LogModel::formattedMessage(const QModelIndex& index) const { if (!index.isValid()) { return ""; } return QString::fromStdString(m_entries[index.row()].formattedMessage); } void LogModel::clear() { beginResetModel(); m_entries.clear(); endResetModel(); } const std::deque& LogModel::entries() const { return m_entries; } void LogModel::onEntryAdded(MOBase::log::Entry e) { bool full = false; if (m_entries.size() > MaxLines) { m_entries.pop_front(); full = true; } const int row = static_cast(m_entries.size()); if (!full) { beginInsertRows(QModelIndex(), row, row + 1); } m_entries.emplace_back(std::move(e)); if (!full) { endInsertRows(); } else { emit dataChanged(createIndex(row, 0), createIndex(row + 1, columnCount({}))); } } QModelIndex LogModel::index(int row, int column, const QModelIndex&) const { return createIndex(row, column, row); } QModelIndex LogModel::parent(const QModelIndex&) const { return QModelIndex(); } int LogModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; else return static_cast(m_entries.size()); } int LogModel::columnCount(const QModelIndex&) const { return 3; } QVariant LogModel::data(const QModelIndex& index, int role) const { using namespace std::chrono; const auto row = static_cast(index.row()); if (row >= m_entries.size()) { return {}; } const auto& e = m_entries[row]; if (role == Qt::DisplayRole) { if (index.column() == 0) { const auto ms = duration_cast(e.time.time_since_epoch()); const auto s = duration_cast(ms); const std::time_t tt = s.count(); const int frac = static_cast(ms.count() % 1000); const auto time = QDateTime::fromSecsSinceEpoch(tt).time().addMSecs(frac); return time.toString("hh:mm:ss.zzz"); } else if (index.column() == 2) { return QString::fromStdString(e.message); } } if (role == Qt::DecorationRole) { if (index.column() == 1) { switch (e.level) { case log::Warning: return QIcon(":/MO/gui/warning"); case log::Error: return QIcon(":/MO/gui/problem"); case log::Debug: return QIcon(":/MO/gui/debug"); case log::Info: return QIcon(":/MO/gui/information"); default: return {}; } } } return QVariant(); } QVariant LogModel::headerData(int, Qt::Orientation, int) const { return {}; } LogList::LogList(QWidget* parent) : QTreeView(parent), m_core(nullptr), m_copyFilter(this, [=](auto&& index) { return LogModel::instance().formattedMessage(index); }) { setModel(&LogModel::instance()); header()->setMinimumSectionSize(0); header()->resizeSection(1, 20); header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); setAutoScroll(true); scrollToBottom(); connect(this, &QWidget::customContextMenuRequested, [&](auto&& pos) { onContextMenu(pos); }); connect(model(), &LogModel::rowsInserted, this, [&] { onNewEntry(); }); connect(model(), &LogModel::dataChanged, this, [&] { onNewEntry(); }); m_timer.setSingleShot(true); connect(&m_timer, &QTimer::timeout, [&] { scrollToBottom(); }); installEventFilter(&m_copyFilter); } void LogList::onNewEntry() { m_timer.start(std::chrono::milliseconds(10)); } void LogList::setCore(OrganizerCore& core) { m_core = &core; } void LogList::copyToClipboard() { std::string s; for (const auto& e : LogModel::instance().entries()) { s += e.formattedMessage + "\n"; } if (!s.empty()) { // last newline s.pop_back(); } QApplication::clipboard()->setText(QString::fromStdString(s)); } void LogList::clear() { static_cast(model())->clear(); } void LogList::openLogsFolder() { QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()); shell::Explore(logsPath); } QMenu* LogList::createMenu(QWidget* parent) { auto* menu = new QMenu(parent); menu->addAction(tr("Copy"), [&] { m_copyFilter.copySelection(); }); menu->addAction(tr("&Copy all"), [&] { copyToClipboard(); }); menu->addSeparator(); menu->addAction(tr("C&lear all"), [&] { clear(); }); menu->addAction(tr("&Open logs folder"), [&] { openLogsFolder(); }); auto* levels = new QMenu(tr("&Level")); menu->addMenu(levels); auto* ag = new QActionGroup(menu); auto addAction = [&](auto&& text, auto&& level) { auto* a = new QAction(text, ag); a->setCheckable(true); a->setChecked(log::getDefault().level() == level); connect(a, &QAction::triggered, [this, level] { if (m_core) { m_core->setLogLevel(level); } }); levels->addAction(a); }; addAction(tr("&Debug"), log::Debug); addAction(tr("&Info"), log::Info); addAction(tr("&Warnings"), log::Warning); addAction(tr("&Errors"), log::Error); return menu; } void LogList::onContextMenu(const QPoint& pos) { auto* menu = createMenu(this); menu->popup(viewport()->mapToGlobal(pos)); } log::Levels convertQtLevel(QtMsgType t) { switch (t) { case QtDebugMsg: return log::Debug; case QtWarningMsg: return log::Warning; case QtCriticalMsg: // fall-through case QtFatalMsg: return log::Error; case QtInfoMsg: // fall-through default: return log::Info; } } void qtLogCallback(QtMsgType type, const QMessageLogContext& context, const QString& message) { std::string_view file = ""; if (type != QtDebugMsg) { if (context.file) { file = context.file; const auto lastSep = file.find_last_of("/\\"); if (lastSep != std::string_view::npos) { file = {context.file + lastSep + 1}; } } } if (file.empty()) { log::log(convertQtLevel(type), "{}", message.toStdString()); } else { log::log(convertQtLevel(type), "[{}:{}] {}", file, context.line, message.toStdString()); } } void logToStdout(bool b) { m_stdout = b; // logging to stdout is already set up in uibase by log::createDefault(), // all it needs is to redirect stdout to the console, which is done by // creating an env::Console object if (m_stdout) { m_console.reset(new env::Console); } else { m_console.reset(); } } void initLogging() { LogModel::create(); log::LoggerConfiguration conf; conf.maxLevel = MOBase::log::Debug; conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$"; conf.utc = true; log::createDefault(conf); log::getDefault().setCallback([](log::Entry e) { LogModel::instance().add(e); }); log::getDefault().addToBlacklist(std::string("\\") + getenv("USERNAME"), "\\USERNAME"); log::getDefault().addToBlacklist(std::string("/") + getenv("USERNAME"), "/USERNAME"); qInstallMessageHandler(qtLogCallback); } bool createAndMakeWritable(const std::wstring& subPath) { QString const dataPath = qApp->property("dataPath").toString(); QString fullPath = dataPath + "/" + QString::fromStdWString(subPath); if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) { QMessageBox::critical(nullptr, QObject::tr("Error"), QObject::tr("Failed to create \"%1\". Your user " "account probably lacks permission.") .arg(fullPath)); return false; } else { return true; } } bool setLogDirectory(const QString& dir) { const auto logFile = dir + "/" + QString::fromStdWString(AppConfig::logPath()) + "/" + QString::fromStdWString(AppConfig::logFileName()); if (!createAndMakeWritable(AppConfig::logPath())) { return false; } log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString())); return true; }