diff --git a/src/Skybolt/SkyboltQt/QtUtil/QtDialogUtil.cpp b/src/Skybolt/SkyboltQt/QtUtil/QtDialogUtil.cpp index 3c18796..d122ba9 100644 --- a/src/Skybolt/SkyboltQt/QtUtil/QtDialogUtil.cpp +++ b/src/Skybolt/SkyboltQt/QtUtil/QtDialogUtil.cpp @@ -40,7 +40,7 @@ QDialog* createDialogNonModal(QWidget* content, const QString& title, QWidget* p QPushButton* button = new QPushButton("Close"); button->setAutoDefault(false); dialog->layout()->addWidget(button); - QObject::connect(button, &QPushButton::pressed, dialog, &QDialog::accept); + QObject::connect(button, &QPushButton::clicked, dialog, &QDialog::accept); return dialog; } diff --git a/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.cpp b/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.cpp new file mode 100644 index 0000000..bbcd6f9 --- /dev/null +++ b/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.cpp @@ -0,0 +1,77 @@ +#include "ErrorLogModel.h" + +#include +#include +#include +#include +#include + +namespace bl = boost::log; + +class LabelLogSink : public bl::sinks::basic_formatted_sink_backend +{ +public: + LabelLogSink(std::function fn) : + mFn(fn) + { + } + + void consume(const bl::record_view& rec, const std::string& str) { + if (auto severityAttribute = rec["Severity"]; severityAttribute) + { + if (auto level = severityAttribute.extract(); level) + { + mFn(*level, QString::fromStdString(str)); + } + } + } + +private: + std::function mFn; +}; + +ErrorLogModel::ErrorLogModel(QObject* parent) : + QObject(parent) +{ +} + +void ErrorLogModel::append(const Item& item) +{ + mItems.push_back(item); + Q_EMIT itemAppended(item); +} + +void ErrorLogModel::clear() +{ + mItems.clear(); + Q_EMIT cleared(); +} + +static ErrorLogModel::Severity toErrorLogModelSeverity(bl::trivial::severity_level level) +{ + switch (level) + { + case bl::trivial::severity_level::warning: + return ErrorLogModel::Severity::Warning; + } + return ErrorLogModel::Severity::Error; +} + +void connectToBoostLogger(QPointer model) +{ + auto sink = boost::make_shared([model = std::move(model)] (bl::trivial::severity_level level, const QString& message) { + if (model) + { + ErrorLogModel::Item item; + item.dateTime = QDateTime::currentDateTime(); + item.severity = toErrorLogModelSeverity(level); + item.message = message; + model->append(item); + } + }); + + using sink_t = bl::sinks::synchronous_sink; + auto sinkWrapper = boost::make_shared(sink); + sinkWrapper->set_filter(bl::trivial::severity >= bl::trivial::warning); + bl::core::get()->add_sink(sinkWrapper); +} diff --git a/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.h b/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.h new file mode 100644 index 0000000..ff0c028 --- /dev/null +++ b/src/Skybolt/SkyboltQt/Widgets/ErrorLogModel.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +class ErrorLogModel : public QObject +{ + Q_OBJECT +public: + ErrorLogModel(QObject* parent = nullptr); + + enum class Severity + { + Warning, + Error + }; + + struct Item + { + QDateTime dateTime; + Severity severity; + QString message; + }; + + void append(const Item& item); + void clear(); + + const std::vector& getItems() const { return mItems; } + + + Q_SIGNAL void itemAppended(const Item& item); + Q_SIGNAL void cleared(); + +private: + std::vector mItems; +}; + +void connectToBoostLogger(QPointer model); diff --git a/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.cpp b/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.cpp new file mode 100644 index 0000000..4dcc591 --- /dev/null +++ b/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.cpp @@ -0,0 +1,76 @@ +#include "ErrorLogWidget.h" + +#include +#include +#include +#include +#include + +ErrorLogWidget::ErrorLogWidget(ErrorLogModel* model, QWidget* parent) : + QWidget(parent) +{ + // Create the table widget with 3 columns + mTableWidget = new QTableWidget(this); + mTableWidget->setColumnCount(3); + mTableWidget->setHorizontalHeaderLabels({"Time", "Severity", "Message"}); + mTableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + mTableWidget->setSortingEnabled(true); // Enable sorting by clicking column headers + mTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // Make the table non-editable + + // Create the "Clear" button + QPushButton* clearButton = new QPushButton("Clear", this); + connect(clearButton, &QPushButton::clicked, model, [model] { + model->clear(); + }); + + // Layout for the dialog + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(mTableWidget); + layout->addWidget(clearButton); + + setLayout(layout); + + connect(model, &ErrorLogModel::itemAppended, this, [this] (const ErrorLogModel::Item& item) { + addItemToTable(item); + }); + + connect(model, &ErrorLogModel::cleared, this, [this] { + mTableWidget->setRowCount(0); + }); + + // Add initial items + for (const auto& item : model->getItems()) + { + addItemToTable(item); + } +} + +static QString toQString(ErrorLogModel::Severity severity) +{ + switch (severity) + { + case ErrorLogModel::Severity::Warning: + return "Warning"; + case ErrorLogModel::Severity::Error: + return "Error"; + } + return ""; +} + +std::unique_ptr createItemWithTooltip(const QString& text) +{ + auto item = std::make_unique(text); + item->setToolTip(text); + return item; +} + +void ErrorLogWidget::addItemToTable(const ErrorLogModel::Item& item) +{ + int row = mTableWidget->rowCount(); + mTableWidget->setRowCount(row + 1); + + // Insert time, severity, and message into the new row + mTableWidget->setItem(row, 0, createItemWithTooltip(item.dateTime.toString("yyyy-MM-dd HH:mm:ss")).release()); + mTableWidget->setItem(row, 1, new QTableWidgetItem(toQString(item.severity))); + mTableWidget->setItem(row, 2, createItemWithTooltip(item.message).release()); +} \ No newline at end of file diff --git a/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.h b/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.h new file mode 100644 index 0000000..f889aea --- /dev/null +++ b/src/Skybolt/SkyboltQt/Widgets/ErrorLogWidget.h @@ -0,0 +1,18 @@ +#pragma once + +#include "ErrorLogModel.h" +#include + +class QTableWidget; + +class ErrorLogWidget : public QWidget +{ +public: + ErrorLogWidget(ErrorLogModel* model, QWidget* parent = nullptr); + +private: + void addItemToTable(const ErrorLogModel::Item& item); + +private: + QTableWidget* mTableWidget; +}; diff --git a/src/Skybolt/SkyboltQt/Widgets/ScenarioObjectCreationToolBar.cpp b/src/Skybolt/SkyboltQt/Widgets/ScenarioObjectCreationToolBar.cpp index c31c801..3f59efb 100644 --- a/src/Skybolt/SkyboltQt/Widgets/ScenarioObjectCreationToolBar.cpp +++ b/src/Skybolt/SkyboltQt/Widgets/ScenarioObjectCreationToolBar.cpp @@ -173,7 +173,7 @@ QToolBar* createScenarioObjectCreationToolBar(ScenarioSelectionModel* selectionM deleteButton->setEnabled(enabled); }); - QObject::connect(deleteButton, &QToolButton::pressed, parent, [selectionModel, scenarioObjectTypes]() + QObject::connect(deleteButton, &QToolButton::clicked, parent, [selectionModel, scenarioObjectTypes]() { if (const auto& object = getFirstSelectedScenarioObject(selectionModel->getSelectedItems()); object) { diff --git a/src/Skybolt/SkyboltQt/Widgets/StatusBar.cpp b/src/Skybolt/SkyboltQt/Widgets/StatusBar.cpp index 5bba945..9220265 100644 --- a/src/Skybolt/SkyboltQt/Widgets/StatusBar.cpp +++ b/src/Skybolt/SkyboltQt/Widgets/StatusBar.cpp @@ -1,4 +1,7 @@ #include "StatusBar.h" +#include "ErrorLogModel.h" +#include "ErrorLogWidget.h" +#include "SkyboltQt/QtUtil/QtDialogUtil.h" #include #include @@ -7,36 +10,13 @@ #include #include #include -#include -#include -#include -#include -#include +#include -namespace bl = boost::log; - -class LabelLogSink : public bl::sinks::basic_formatted_sink_backend +void addErrorLogStatusBar(QStatusBar& bar, ErrorLogModel* model) { -public: - LabelLogSink(std::function fn) : - mFn(fn) - { - } - - void consume(const bl::record_view& rec, const std::string& str) { - mFn(QString::fromStdString(str)); - } - -private: - std::function mFn; + assert(model); -private: - QLabel* mLabel; -}; - -void addErrorLogStatusBar(QStatusBar& bar) -{ auto widget = new QWidget(&bar); auto layout = new QHBoxLayout(widget); layout->setMargin(0); @@ -56,39 +36,33 @@ void addErrorLogStatusBar(QStatusBar& bar) infoButton->setVisible(false); layout->addWidget(infoButton); - auto clearButton = new QToolButton(&bar); - clearButton->setIcon(style->standardIcon(QStyle::SP_TitleBarCloseButton)); - clearButton->setToolTip("Close"); - clearButton->setFixedHeight(fm.height()); - clearButton->setVisible(false); - layout->addWidget(clearButton); - - QObject::connect(infoButton, &QToolButton::pressed, [parent = &bar, infoButton] { - QMessageBox::about(parent, "", infoButton->property("messageText").toString()); - }); - - QObject::connect(clearButton, &QToolButton::pressed, [label, infoButton, clearButton] { - label->setText(""); - infoButton->setVisible(false); - clearButton->setVisible(false); + QObject::connect(infoButton, &QToolButton::clicked, model, [parent = &bar, infoButton, model] { + auto logWidget = new ErrorLogWidget(model, parent); + QDialog* dialog = createDialogNonModal(logWidget, "Error Log"); + dialog->resize(800, 500); + dialog->show(); }); bar.addPermanentWidget(widget); - auto sink = boost::make_shared([label, infoButton, clearButton] (const QString& message) { - QString singleLineMessage = message; + auto sink = [label, infoButton] (const ErrorLogModel::Item& item) { + QString singleLineMessage = item.message; singleLineMessage = singleLineMessage.replace('\n', ' '); singleLineMessage.resize(std::min(singleLineMessage.size(), 100)); singleLineMessage += "..."; label->setText(singleLineMessage); - infoButton->setProperty("messageText", message); - infoButton->setVisible(!message.isEmpty()); - clearButton->setVisible(!message.isEmpty()); - }); + infoButton->setVisible(!item.message.isEmpty()); + }; - using sink_t = bl::sinks::synchronous_sink; - auto sinkWrapper = boost::make_shared(sink); - sinkWrapper->set_filter(bl::trivial::severity >= bl::trivial::error); - bl::core::get()->add_sink(sinkWrapper); -} \ No newline at end of file + QObject::connect(model, &ErrorLogModel::itemAppended, &bar, sink); + if (!model->getItems().empty()) + { + sink(model->getItems().back()); + } + + QObject::connect(model, &ErrorLogModel::cleared, &bar, [label, infoButton] { + label->setText(""); + infoButton->setVisible(false); + }); +} diff --git a/src/Skybolt/SkyboltQt/Widgets/StatusBar.h b/src/Skybolt/SkyboltQt/Widgets/StatusBar.h index 7f7b1cc..aef1064 100644 --- a/src/Skybolt/SkyboltQt/Widgets/StatusBar.h +++ b/src/Skybolt/SkyboltQt/Widgets/StatusBar.h @@ -1,5 +1,6 @@ #pragma once +class ErrorLogModel; class QStatusBar; -void addErrorLogStatusBar(QStatusBar& bar); \ No newline at end of file +void addErrorLogStatusBar(QStatusBar& bar, ErrorLogModel* model); \ No newline at end of file diff --git a/src/Skybolt/SkyboltQt/Widgets/TimeRateDialog.cpp b/src/Skybolt/SkyboltQt/Widgets/TimeRateDialog.cpp index 6a6b991..8a1f9d8 100644 --- a/src/Skybolt/SkyboltQt/Widgets/TimeRateDialog.cpp +++ b/src/Skybolt/SkyboltQt/Widgets/TimeRateDialog.cpp @@ -59,19 +59,19 @@ TimeRateDialog::TimeRateDialog(double initialRate, QWidget* parent) : toolBar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); setMaximumWidth(toolBar->width()); - connect(realtimeButton, &QPushButton::pressed, this, [this, customRateLineEdit] { + connect(realtimeButton, &QPushButton::clicked, this, [this, customRateLineEdit] { mRate = 1; customRateLineEdit->setText(QString::number(mRate)); emit rateChanged(mRate); }); - connect(slowDownButton, &QPushButton::pressed, this, [this, customRateLineEdit] { + connect(slowDownButton, &QPushButton::clicked, this, [this, customRateLineEdit] { mRate /= 2; customRateLineEdit->setText(QString::number(mRate)); emit rateChanged(mRate); }); - connect(speedUpButton, &QPushButton::pressed, this, [this, customRateLineEdit] { + connect(speedUpButton, &QPushButton::clicked, this, [this, customRateLineEdit] { mRate *= 2; customRateLineEdit->setText(QString::number(mRate)); emit rateChanged(mRate); diff --git a/src/SkyboltExamples/SkyboltQtApp/main.cpp b/src/SkyboltExamples/SkyboltQtApp/main.cpp index 9e6cb2f..edae330 100644 --- a/src/SkyboltExamples/SkyboltQtApp/main.cpp +++ b/src/SkyboltExamples/SkyboltQtApp/main.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,10 @@ class Application : public QApplication { setStyle(new DarkStyle); + // Create model for logging application warnings and errors + auto errorLogModel = new ErrorLogModel(this); + connectToBoostLogger(errorLogModel); + // Create engine { QSettings settings(QApplication::applicationName()); @@ -126,8 +131,8 @@ class Application : public QApplication // Warn user if python is not available if (!isPythonOnPath(PYTHON_VERSION_MAJOR, PYTHON_VERSION_MINOR)) { - QMessageBox::warning(nullptr, "Warning", QString("Python %1.%2 not found in PATH environment variable. Python functionality will be disabled.") - .arg(PYTHON_VERSION_MAJOR).arg(PYTHON_VERSION_MINOR)); + BOOST_LOG_TRIVIAL(warning) << QString("Python %1.%2 not found in PATH environment variable. Python functionality will be disabled.") + .arg(PYTHON_VERSION_MAJOR).arg(PYTHON_VERSION_MINOR).toStdString(); } #endif @@ -188,7 +193,7 @@ class Application : public QApplication c.engineRoot = mEngineRoot; return c; }())); - addErrorLogStatusBar(*mMainWindow->statusBar()); + addErrorLogStatusBar(*mMainWindow->statusBar(), errorLogModel); enableDarkTitleBar(mMainWindow->winId()); auto selectionModel = new ScenarioSelectionModel(mMainWindow.get()); @@ -450,7 +455,7 @@ class Application : public QApplication auto closeButton = new QPushButton("Close", &dialog); layout.addWidget(closeButton); - QObject::connect(closeButton, &QPushButton::pressed, &dialog, &QDialog::accept); + QObject::connect(closeButton, &QPushButton::clicked, &dialog, &QDialog::accept); dialog.exec(); }