From ef4a7b1212ee356e52e23b75e2b6115f60a909a8 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 13 Dec 2024 23:48:12 +0100 Subject: [PATCH 01/22] [feature/redesign] Tree based effects dialog with custom effects --- resources/icons/table/remove_black.svg | 17 +- resources/icons/table/remove_white.svg | 17 +- src/handler/file/CMakeLists.txt | 2 + src/handler/file/EffectFileHandler.cpp | 54 ++++++ src/handler/file/EffectFileHandler.hpp | 36 ++++ src/ui/table/dialog/AddCustomEffectDialog.cpp | 37 ++++ src/ui/table/dialog/AddCustomEffectDialog.hpp | 22 +++ src/ui/table/dialog/CMakeLists.txt | 2 + src/ui/table/dialog/StatusEffectDialog.cpp | 176 ++++++++++++++++-- src/ui/table/dialog/StatusEffectDialog.hpp | 29 ++- test/CMakeLists.txt | 1 + test/handler/EffectFileHandlerTest.cpp | 55 ++++++ 12 files changed, 403 insertions(+), 45 deletions(-) create mode 100644 src/handler/file/EffectFileHandler.cpp create mode 100644 src/handler/file/EffectFileHandler.hpp create mode 100644 src/ui/table/dialog/AddCustomEffectDialog.cpp create mode 100644 src/ui/table/dialog/AddCustomEffectDialog.hpp create mode 100644 test/handler/EffectFileHandlerTest.cpp diff --git a/resources/icons/table/remove_black.svg b/resources/icons/table/remove_black.svg index e9ab77a1..3ba8333e 100644 --- a/resources/icons/table/remove_black.svg +++ b/resources/icons/table/remove_black.svg @@ -1,20 +1,11 @@ - - - - - - - - - - + diff --git a/resources/icons/table/remove_white.svg b/resources/icons/table/remove_white.svg index 82e7f1fa..d322bd34 100644 --- a/resources/icons/table/remove_white.svg +++ b/resources/icons/table/remove_white.svg @@ -1,20 +1,11 @@ - - - - - - - - - - + diff --git a/src/handler/file/CMakeLists.txt b/src/handler/file/CMakeLists.txt index 14409ad0..970a1cb8 100644 --- a/src/handler/file/CMakeLists.txt +++ b/src/handler/file/CMakeLists.txt @@ -9,6 +9,8 @@ target_sources(fileHandler INTERFACE ${CMAKE_CURRENT_LIST_DIR}/BaseFileHandler.hpp ${CMAKE_CURRENT_LIST_DIR}/CharFileHandler.cpp ${CMAKE_CURRENT_LIST_DIR}/CharFileHandler.hpp + ${CMAKE_CURRENT_LIST_DIR}/EffectFileHandler.cpp + ${CMAKE_CURRENT_LIST_DIR}/EffectFileHandler.hpp ${CMAKE_CURRENT_LIST_DIR}/TableFileHandler.cpp ${CMAKE_CURRENT_LIST_DIR}/TableFileHandler.hpp ) diff --git a/src/handler/file/EffectFileHandler.cpp b/src/handler/file/EffectFileHandler.cpp new file mode 100644 index 00000000..344ffbe9 --- /dev/null +++ b/src/handler/file/EffectFileHandler.cpp @@ -0,0 +1,54 @@ +#include "EffectFileHandler.hpp" + +#include +#include +#include + +EffectFileHandler::EffectFileHandler() +{ + // Create a subdir to save the tables into + QDir dir(QDir::currentPath()); + if (!dir.exists(QDir::currentPath() + "/effects")) { + dir.mkdir("effects"); + } + m_directoryString = QDir::currentPath() + "/effects/"; +} + + +bool +EffectFileHandler::writeToFile(const QString& name) const +{ + QJsonObject effectObject; + effectObject["name"] = name; + + // Write to file + const auto byteArray = QJsonDocument(effectObject).toJson(); + QFile fileOut(m_directoryString + "/" + name + ".effect"); + fileOut.open(QIODevice::WriteOnly); + return fileOut.write(byteArray); +} + + +bool +EffectFileHandler::removeEffect(const QString& effectName) +{ + QFile fileOut(m_directoryString + effectName); + if (!fileOut.exists()) { + return false; + } + return fileOut.remove(); +}; + + +int +EffectFileHandler::getStatus(const QString& fileName) +{ + return BaseFileHandler::getStatus(m_directoryString + fileName); +} + + +bool +EffectFileHandler::checkFileFormat() const +{ + return !m_fileData.empty() && m_fileData.contains("name"); +} diff --git a/src/handler/file/EffectFileHandler.hpp b/src/handler/file/EffectFileHandler.hpp new file mode 100644 index 00000000..111495f7 --- /dev/null +++ b/src/handler/file/EffectFileHandler.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "BaseFileHandler.hpp" +#include "CharacterHandler.hpp" + +// This class handles the saving and opening of effect templates +class EffectFileHandler : public BaseFileHandler { +public: + EffectFileHandler(); + + // Write effect + [[nodiscard]] bool + writeToFile(const QString& name) const; + + // Remove effect + [[nodiscard]] bool + removeEffect(const QString& effectName); + + // Open a saved table + [[nodiscard]] int + getStatus(const QString& fileName) override; + + const QString& + getDirectoryString() + { + return m_directoryString; + } + +private: + // Checks if a loaded lcm file is in the right format + [[nodiscard]] bool + checkFileFormat() const override; + +private: + QString m_directoryString; +}; diff --git a/src/ui/table/dialog/AddCustomEffectDialog.cpp b/src/ui/table/dialog/AddCustomEffectDialog.cpp new file mode 100644 index 00000000..9c9d6e2d --- /dev/null +++ b/src/ui/table/dialog/AddCustomEffectDialog.cpp @@ -0,0 +1,37 @@ +#include "AddCustomEffectDialog.hpp" + +#include "UtilsGeneral.hpp" + +#include +#include +#include +#include +#include + +AddCustomEffectDialog::AddCustomEffectDialog(const QList& otherEffects, QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Add Custom Effect")); + + auto* const lineEdit = new QLineEdit; + + auto *const buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + auto* const mainLayout = new QVBoxLayout; + mainLayout->addWidget(lineEdit); + mainLayout->addWidget(buttonBox); + + setLayout(mainLayout); + + connect(buttonBox, &QDialogButtonBox::accepted, this, [this, otherEffects, lineEdit] { + if (otherEffects.contains(lineEdit->text())) { + Utils::General::displayWarningMessageBox(this, tr("Effect already exists!"), + tr("The effect already exists. Please provide a different name!")); + return; + } + + m_name = lineEdit->text(); + QDialog::accept(); + }); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/src/ui/table/dialog/AddCustomEffectDialog.hpp b/src/ui/table/dialog/AddCustomEffectDialog.hpp new file mode 100644 index 00000000..e07c78c4 --- /dev/null +++ b/src/ui/table/dialog/AddCustomEffectDialog.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +// Dialog used to add effects +class AddCustomEffectDialog : public QDialog { + Q_OBJECT + +public: + explicit + AddCustomEffectDialog(const QList& otherEffects, + QWidget* parent = 0); + + [[nodiscard]] QString& + getName() + { + return m_name; + } + +private: + QString m_name; +}; diff --git a/src/ui/table/dialog/CMakeLists.txt b/src/ui/table/dialog/CMakeLists.txt index 2f76bdde..0e2cacc8 100644 --- a/src/ui/table/dialog/CMakeLists.txt +++ b/src/ui/table/dialog/CMakeLists.txt @@ -9,6 +9,8 @@ target_include_directories (dialog target_sources(dialog INTERFACE ${CMAKE_CURRENT_LIST_DIR}/AddCharacterDialog.hpp ${CMAKE_CURRENT_LIST_DIR}/AddCharacterDialog.cpp + ${CMAKE_CURRENT_LIST_DIR}/AddCustomEffectDialog.hpp + ${CMAKE_CURRENT_LIST_DIR}/AddCustomEffectDialog.cpp ${CMAKE_CURRENT_LIST_DIR}/ChangeStatDialog.hpp ${CMAKE_CURRENT_LIST_DIR}/ChangeStatDialog.cpp ${CMAKE_CURRENT_LIST_DIR}/StatusEffectData.hpp diff --git a/src/ui/table/dialog/StatusEffectDialog.cpp b/src/ui/table/dialog/StatusEffectDialog.cpp index 2850a41d..a49233f9 100644 --- a/src/ui/table/dialog/StatusEffectDialog.cpp +++ b/src/ui/table/dialog/StatusEffectDialog.cpp @@ -1,18 +1,22 @@ #include "StatusEffectDialog.hpp" +#include "AddCustomEffectDialog.hpp" #include "RuleSettings.hpp" #include "StatusEffectData.hpp" +#include "UtilsGeneral.hpp" #include -#include #include +#include +#include #include #include #include -#include #include #include #include +#include +#include #include StatusEffectDialog::StatusEffectDialog(const RuleSettings& RuleSettings, QWidget *parent) : @@ -20,7 +24,7 @@ StatusEffectDialog::StatusEffectDialog(const RuleSettings& RuleSettings, QWidget { setWindowTitle(tr("Add Status Effect(s)")); - m_lineEdit = new QLineEdit(this); + m_lineEdit = new QLineEdit; auto *const shortcut = new QShortcut(QKeySequence::Find, this); connect(shortcut, &QShortcut::activated, this, [this] () { m_lineEdit->setFocus(Qt::ShortcutFocusReason); @@ -30,11 +34,56 @@ StatusEffectDialog::StatusEffectDialog(const RuleSettings& RuleSettings, QWidget m_lineEdit->setToolTip(tr("Selected list items are returned as effect.\n" "If nothing is selected, the entered text will be returned.")); - m_listWidget = new QListWidget(this); - m_listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_effectFileHandler = std::make_unique(); + + m_treeWidget = new QTreeWidget; + m_treeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_treeWidget->setHeaderHidden(true); + + // Apply standard effects + auto* const standardItem = new QTreeWidgetItem; + standardItem->setText(COL_NAME, "Standard Effects:"); + m_treeWidget->addTopLevelItem(standardItem); + + QList items; for (const auto effects = StatusEffectData::getEffectList(m_ruleSettings.ruleset); const auto& effect : effects) { - m_listWidget->addItem(new QListWidgetItem(effect)); + auto* const item = new QTreeWidgetItem; + item->setText(COL_NAME, effect); + items.append(item); + } + standardItem->addChildren(items); + items.clear(); + + // Apply custom effects + m_customHeaderItem = new QTreeWidgetItem; + m_customHeaderItem->setText(COL_NAME, "Custom Effects:"); + m_treeWidget->addTopLevelItem(m_customHeaderItem); + + QDirIterator it(m_effectFileHandler->getDirectoryString(), { "*.effect" }, QDir::Files); + while (it.hasNext()) { + it.next(); + + if (const auto code = m_effectFileHandler->getStatus(it.fileName()); code == 0) { + const auto effectObject = m_effectFileHandler->getData(); + auto* const item = new QTreeWidgetItem; + item->setText(COL_NAME, effectObject["name"].toString()); + items.append(item); + } } + m_customHeaderItem->addChildren(items); + m_treeWidget->expandAll(); + + m_removeEffectButton = new QToolButton; + m_removeEffectButton->setEnabled(false); + m_removeEffectButton->setToolTip(tr("Remove a custom effect.")); + m_addEffectButton = new QToolButton; + m_addEffectButton->setToolTip(tr("Add a custom effect.")); + setButtonIcons(); + + auto* const effectButtonLayout = new QHBoxLayout; + effectButtonLayout->addStretch(); + effectButtonLayout->addWidget(m_removeEffectButton); + effectButtonLayout->addWidget(m_addEffectButton); m_checkBox = new QCheckBox(tr("Permanent")); m_checkBox->setTristate(false); @@ -59,7 +108,8 @@ StatusEffectDialog::StatusEffectDialog(const RuleSettings& RuleSettings, QWidget auto *const layout = new QVBoxLayout(this); layout->addWidget(m_lineEdit); - layout->addWidget(m_listWidget); + layout->addWidget(m_treeWidget); + layout->addLayout(effectButtonLayout); layout->addLayout(spinBoxLayout); layout->addWidget(buttonBox); setLayout(layout); @@ -67,29 +117,99 @@ StatusEffectDialog::StatusEffectDialog(const RuleSettings& RuleSettings, QWidget connect(m_lineEdit, &QLineEdit::textChanged, this, [this] () { findEffect(m_lineEdit->text()); }); + connect(m_treeWidget, &QTreeWidget::itemDoubleClicked, this, [this, standardItem] (QTreeWidgetItem *item, int /* column */) { + if (item == standardItem || item == m_customHeaderItem) { + return; + } + + createEffect(item->text(COL_NAME)); + QDialog::accept(); + }); + connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &StatusEffectDialog::setRemoveButtonEnabling); + connect(m_addEffectButton, &QPushButton::clicked, this, &StatusEffectDialog::addEffectButtonClicked); + connect(m_removeEffectButton, &QPushButton::clicked, this, &StatusEffectDialog::removeEffectButtonClicked); connect(m_checkBox, &QCheckBox::stateChanged, this, [this, spinBoxLabel] { spinBoxLabel->setEnabled(m_checkBox->checkState() != Qt::Checked); m_spinBox->setEnabled(m_checkBox->checkState() != Qt::Checked); }); - connect(m_listWidget, &QListWidget::itemDoubleClicked, this, [this] (QListWidgetItem *item) { - createEffect(item->text()); - QDialog::accept(); - }); connect(okButton, &QPushButton::clicked, this, &StatusEffectDialog::okButtonClicked); connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); } +void +StatusEffectDialog::setRemoveButtonEnabling() +{ + const auto& selectedItems = m_treeWidget->selectedItems(); + if (selectedItems.size() != 1) { + m_removeEffectButton->setEnabled(false); + return; + } + + QList customChildrenItem; + for (auto i = 0; i < m_customHeaderItem->childCount(); i++) { + customChildrenItem.append(m_customHeaderItem->child(i)); + } + + for (auto i = 0; i < selectedItems.count(); i++) { + if (!customChildrenItem.contains(selectedItems.at(i))) { + m_removeEffectButton->setEnabled(false); + return; + } + } + m_removeEffectButton->setEnabled(true); +} + + +void +StatusEffectDialog::removeEffectButtonClicked() +{ + const auto& selectedItems = m_treeWidget->selectedItems(); + if (selectedItems.size() != 1) { + return; + } + + if (!m_effectFileHandler->removeEffect(selectedItems.at(0)->text(COL_NAME) + ".effect")) { + Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("Could not remove Effect!")); + return; + } + delete selectedItems.at(0); +} + + +void +StatusEffectDialog::addEffectButtonClicked() +{ + QList otherEffects; + for (auto i = 0; i < m_customHeaderItem->childCount(); i++) { + auto* const customEffectItem = m_customHeaderItem->child(i); + otherEffects.append(customEffectItem->text(COL_NAME)); + } + + if (auto *const dialog = new AddCustomEffectDialog(otherEffects, this); dialog->exec() == QDialog::Accepted) { + const auto effectName = dialog->getName(); + if (!m_effectFileHandler->writeToFile(effectName)) { + Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("The Effect could not be saved!")); + return; + } + + auto* const newItem = new QTreeWidgetItem; + newItem->setText(COL_NAME, effectName); + m_customHeaderItem->addChild(newItem); + } +} + + void StatusEffectDialog::okButtonClicked() { // If nothing is selected, add the line edit text as status effect - if (m_listWidget->selectedItems().empty() && !m_lineEdit->text().isEmpty()) { + if (m_treeWidget->selectedItems().empty() && !m_lineEdit->text().isEmpty()) { createEffect(m_lineEdit->text()); } else { // Otherwise, add the effect in the list - for (auto* const item : m_listWidget->selectedItems()) { - createEffect(item->text()); + for (auto* const item : m_treeWidget->selectedItems()) { + createEffect(item->text(COL_NAME)); } } @@ -112,8 +232,30 @@ void StatusEffectDialog::findEffect(const QString& filter) { // Hide effects not containing the filter - for (int i = 0; i < m_listWidget->count(); ++i) { - auto *item = m_listWidget->item(i); - item->setHidden(!item->text().contains(filter, Qt::CaseInsensitive)); + QTreeWidgetItemIterator it(m_treeWidget); + while (*it) { + if ((*it)->childCount() == 0) { + (*it)->setHidden(!(*it)->text(COL_NAME).contains(filter, Qt::CaseInsensitive)); + } + ++it; + } +} + + +void +StatusEffectDialog::setButtonIcons() +{ + const auto isSystemInDarkMode = Utils::General::isSystemInDarkMode(); + m_removeEffectButton->setIcon(isSystemInDarkMode ? QIcon(":/icons/table/remove_white.svg") : QIcon(":/icons/table/remove_black.svg")); + m_addEffectButton->setIcon(isSystemInDarkMode ? QIcon(":/icons/table/add_white.svg") : QIcon(":/icons/table/add_black.svg")); +} + + +bool +StatusEffectDialog::event(QEvent *event) +{ + [[unlikely]] if (event->type() == QEvent::ApplicationPaletteChange || event->type() == QEvent::PaletteChange) { + setButtonIcons(); } + return QWidget::event(event); } diff --git a/src/ui/table/dialog/StatusEffectDialog.hpp b/src/ui/table/dialog/StatusEffectDialog.hpp index 51de83dc..f001af58 100644 --- a/src/ui/table/dialog/StatusEffectDialog.hpp +++ b/src/ui/table/dialog/StatusEffectDialog.hpp @@ -1,13 +1,16 @@ #pragma once #include "AdditionalInfoData.hpp" +#include "EffectFileHandler.hpp" #include #include +#include class QCheckBox; class QLineEdit; -class QListWidget; +class QTreeWidget; +class QToolButton; class QSpinBox; class RuleSettings; @@ -28,6 +31,15 @@ class StatusEffectDialog : public QDialog { } private slots: + void + setRemoveButtonEnabling(); + + void + addEffectButtonClicked(); + + void + removeEffectButtonClicked(); + void okButtonClicked(); @@ -38,13 +50,26 @@ private slots: void findEffect(const QString& filter); + void + setButtonIcons(); + + bool + event(QEvent* event); + private: - QPointer m_listWidget; QPointer m_lineEdit; + QPointer m_treeWidget; + QPointer m_removeEffectButton; + QPointer m_addEffectButton; QPointer m_checkBox; QPointer m_spinBox; + QTreeWidgetItem* m_customHeaderItem; + + std::unique_ptr m_effectFileHandler; QVector m_effects; const RuleSettings& m_ruleSettings; + + static constexpr int COL_NAME = 0; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8cc7bbb..d1b4973d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(tests ${CMAKE_CURRENT_LIST_DIR}/handler/CharacterHandlerTest.cpp ${CMAKE_CURRENT_LIST_DIR}/handler/CharFileHandlerTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/handler/EffectFileHandlerTest.cpp ${CMAKE_CURRENT_LIST_DIR}/handler/TableFileHandlerTest.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/settings/SettingsTest.cpp diff --git a/test/handler/EffectFileHandlerTest.cpp b/test/handler/EffectFileHandlerTest.cpp new file mode 100644 index 00000000..0d2d9e4e --- /dev/null +++ b/test/handler/EffectFileHandlerTest.cpp @@ -0,0 +1,55 @@ +#include "EffectFileHandler.hpp" + +#ifdef CATCH2_V3 +#include +#else +#include +#endif + +#include +#include +#include + +TEST_CASE("EffectFileHandler Testing", "[EffectFileHandler]") { + auto const effectFileHandler = std::make_unique(); + const auto effectSaved = effectFileHandler->writeToFile("Test Effect"); + + QDir dir; + const auto effectPath = dir.currentPath() + "/effects/Test Effect.effect"; + + SECTION("Effect successfully saved") { + REQUIRE(effectSaved == true); + REQUIRE(dir.exists(effectPath)); + } + SECTION("File format and content correct") { + const auto codeCSVStatus = effectFileHandler->getStatus("Test Effect.effect"); + REQUIRE(codeCSVStatus == 0); + + const auto& jsonObject = effectFileHandler->getData(); + REQUIRE(jsonObject.value("name").toString() == "Test Effect"); + } + + SECTION("Broken table") { + // Incomplete json object + QJsonObject jsonObject; + jsonObject["name"] = 2; + + // Write to file + auto byteArray = QJsonDocument(jsonObject).toJson(); + QFile fileOut(dir.currentPath() + "/effects/Broken Effect.effect"); + fileOut.open(QIODevice::WriteOnly); + fileOut.write(byteArray); + + REQUIRE(effectFileHandler->getStatus("Broken Effect.effect") == 1); + dir.remove(dir.currentPath() + "/effects/Broken Effect.effect"); + } + SECTION("Non-existent file") { + const auto codeCSVStatus = effectFileHandler->getStatus("nonexisting.effect"); + REQUIRE(codeCSVStatus == 2); + } + SECTION("Check file removal") { + const auto fileRemoved = effectFileHandler->removeEffect("Test Effect.effect"); + REQUIRE(fileRemoved == true); + REQUIRE(!dir.exists(effectPath)); + } +} From ea89a8c3a38b8dd05d7ced125b58ca97b1cace6f Mon Sep 17 00:00:00 2001 From: Bakefish Date: Sun, 15 Dec 2024 17:54:41 +0100 Subject: [PATCH 02/22] [refactor] Reduce boilerplate code for file handlers --- src/handler/file/BaseFileHandler.cpp | 10 ++++++++ src/handler/file/BaseFileHandler.hpp | 4 ++++ src/handler/file/CharFileHandler.cpp | 19 +-------------- src/handler/file/CharFileHandler.hpp | 4 ---- src/handler/file/EffectFileHandler.cpp | 19 +-------------- src/handler/file/EffectFileHandler.hpp | 4 ---- src/handler/file/TableFileHandler.cpp | 21 ++++++----------- src/ui/table/dialog/StatusEffectDialog.cpp | 4 +++- .../table/dialog/template/TemplatesWidget.cpp | 4 +++- src/utils/CMakeLists.txt | 2 ++ src/utils/UtilsFiles.cpp | 16 +++++++++++++ src/utils/UtilsFiles.hpp | 11 +++++++++ test/handler/CharFileHandlerTest.cpp | 15 +++--------- test/handler/EffectFileHandlerTest.cpp | 17 ++++---------- test/handler/TableFileHandlerTest.cpp | 8 +------ test/utils/GeneralUtilsTest.cpp | 3 --- test/utils/UtilsFilesTest.cpp | 23 +++++++++++++++++++ 17 files changed, 89 insertions(+), 95 deletions(-) create mode 100644 src/utils/UtilsFiles.cpp create mode 100644 src/utils/UtilsFiles.hpp create mode 100644 test/utils/UtilsFilesTest.cpp diff --git a/src/handler/file/BaseFileHandler.cpp b/src/handler/file/BaseFileHandler.cpp index ee6a65ee..96cab442 100644 --- a/src/handler/file/BaseFileHandler.cpp +++ b/src/handler/file/BaseFileHandler.cpp @@ -19,3 +19,13 @@ BaseFileHandler::getStatus(const QString& fileName) // Correct or false format return !checkFileFormat(); } + + +bool +BaseFileHandler::writeJsonObjectToFile(const QJsonObject& object, const QString& fileName) const +{ + const auto byteArray = QJsonDocument(object).toJson(); + QFile fileOut(fileName); + fileOut.open(QIODevice::WriteOnly); + return fileOut.write(byteArray) != -1; +} diff --git a/src/handler/file/BaseFileHandler.hpp b/src/handler/file/BaseFileHandler.hpp index 010fa448..bd31020b 100644 --- a/src/handler/file/BaseFileHandler.hpp +++ b/src/handler/file/BaseFileHandler.hpp @@ -10,6 +10,10 @@ class BaseFileHandler { [[nodiscard]] virtual int getStatus(const QString& fileName); + [[maybe_unused]] bool + writeJsonObjectToFile(const QJsonObject& object, + const QString& fileName) const; + [[nodiscard]] QJsonObject& getData() { diff --git a/src/handler/file/CharFileHandler.cpp b/src/handler/file/CharFileHandler.cpp index 7180123e..686bb986 100644 --- a/src/handler/file/CharFileHandler.cpp +++ b/src/handler/file/CharFileHandler.cpp @@ -1,8 +1,6 @@ #include "CharFileHandler.hpp" #include -#include -#include CharFileHandler::CharFileHandler() { @@ -26,25 +24,10 @@ CharFileHandler::writeToFile(const CharacterHandler::Character &character) const characterObject["is_enemy"] = character.isEnemy; characterObject["additional_info"] = character.additionalInfoData.mainInfoText; - // Write to file - const auto byteArray = QJsonDocument(characterObject).toJson(); - QFile fileOut(m_directoryString + "/" + character.name + ".char"); - fileOut.open(QIODevice::WriteOnly); - return fileOut.write(byteArray); + return BaseFileHandler::writeJsonObjectToFile(characterObject, m_directoryString + "/" + character.name + ".char"); } -bool -CharFileHandler::removeCharacter(const QString& fileName) -{ - QFile fileOut(m_directoryString + fileName); - if (!fileOut.exists()) { - return false; - } - return fileOut.remove(); -}; - - int CharFileHandler::getStatus(const QString& fileName) { diff --git a/src/handler/file/CharFileHandler.hpp b/src/handler/file/CharFileHandler.hpp index ed25418a..16927b52 100644 --- a/src/handler/file/CharFileHandler.hpp +++ b/src/handler/file/CharFileHandler.hpp @@ -12,10 +12,6 @@ class CharFileHandler : public BaseFileHandler { [[nodiscard]] bool writeToFile(const CharacterHandler::Character& character) const; - // Remove a saved character file - [[nodiscard]] bool - removeCharacter(const QString& fileName); - // Open a saved table [[nodiscard]] int getStatus(const QString& fileName) override; diff --git a/src/handler/file/EffectFileHandler.cpp b/src/handler/file/EffectFileHandler.cpp index 344ffbe9..f021e44d 100644 --- a/src/handler/file/EffectFileHandler.cpp +++ b/src/handler/file/EffectFileHandler.cpp @@ -1,8 +1,6 @@ #include "EffectFileHandler.hpp" #include -#include -#include EffectFileHandler::EffectFileHandler() { @@ -21,25 +19,10 @@ EffectFileHandler::writeToFile(const QString& name) const QJsonObject effectObject; effectObject["name"] = name; - // Write to file - const auto byteArray = QJsonDocument(effectObject).toJson(); - QFile fileOut(m_directoryString + "/" + name + ".effect"); - fileOut.open(QIODevice::WriteOnly); - return fileOut.write(byteArray); + return BaseFileHandler::writeJsonObjectToFile(effectObject, m_directoryString + "/" + name + ".effect"); } -bool -EffectFileHandler::removeEffect(const QString& effectName) -{ - QFile fileOut(m_directoryString + effectName); - if (!fileOut.exists()) { - return false; - } - return fileOut.remove(); -}; - - int EffectFileHandler::getStatus(const QString& fileName) { diff --git a/src/handler/file/EffectFileHandler.hpp b/src/handler/file/EffectFileHandler.hpp index 111495f7..b639c2e6 100644 --- a/src/handler/file/EffectFileHandler.hpp +++ b/src/handler/file/EffectFileHandler.hpp @@ -12,10 +12,6 @@ class EffectFileHandler : public BaseFileHandler { [[nodiscard]] bool writeToFile(const QString& name) const; - // Remove effect - [[nodiscard]] bool - removeEffect(const QString& effectName); - // Open a saved table [[nodiscard]] int getStatus(const QString& fileName) override; diff --git a/src/handler/file/TableFileHandler.cpp b/src/handler/file/TableFileHandler.cpp index 403cdde9..a2193f8a 100644 --- a/src/handler/file/TableFileHandler.cpp +++ b/src/handler/file/TableFileHandler.cpp @@ -2,9 +2,6 @@ #include "AdditionalInfoData.hpp" -#include -#include - bool TableFileHandler::writeToFile( const QVector >& tableData, @@ -15,11 +12,11 @@ TableFileHandler::writeToFile( bool rollAutomatically) const { // Main combat stats - QJsonObject lcmFile; - lcmFile["row_entered"] = (int) rowEntered; - lcmFile["round_counter"] = (int) roundCounter; - lcmFile["ruleset"] = (int) ruleset; - lcmFile["roll_automatically"] = rollAutomatically; + QJsonObject tableObject; + tableObject["row_entered"] = (int) rowEntered; + tableObject["round_counter"] = (int) roundCounter; + tableObject["ruleset"] = (int) ruleset; + tableObject["roll_automatically"] = rollAutomatically; QJsonObject charactersObject; for (auto i = 0; const auto& row : tableData) { @@ -51,13 +48,9 @@ TableFileHandler::writeToFile( charactersObject[QString::number(i++)] = singleCharacterObject; } - lcmFile["characters"] = charactersObject; + tableObject["characters"] = charactersObject; - // Write to file - auto byteArray = QJsonDocument(lcmFile).toJson(); - QFile fileOut(fileName); - fileOut.open(QIODevice::WriteOnly); - return fileOut.write(byteArray) != -1; + return BaseFileHandler::writeJsonObjectToFile(tableObject, fileName); } diff --git a/src/ui/table/dialog/StatusEffectDialog.cpp b/src/ui/table/dialog/StatusEffectDialog.cpp index a49233f9..92777a9a 100644 --- a/src/ui/table/dialog/StatusEffectDialog.cpp +++ b/src/ui/table/dialog/StatusEffectDialog.cpp @@ -3,6 +3,7 @@ #include "AddCustomEffectDialog.hpp" #include "RuleSettings.hpp" #include "StatusEffectData.hpp" +#include "UtilsFiles.hpp" #include "UtilsGeneral.hpp" #include @@ -169,7 +170,8 @@ StatusEffectDialog::removeEffectButtonClicked() return; } - if (!m_effectFileHandler->removeEffect(selectedItems.at(0)->text(COL_NAME) + ".effect")) { + if (const auto effectRemoved = Utils::Files::removeFile(m_effectFileHandler->getDirectoryString() + selectedItems.at(0)->text(COL_NAME) + ".effect"); + !effectRemoved) { Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("Could not remove Effect!")); return; } diff --git a/src/ui/table/dialog/template/TemplatesWidget.cpp b/src/ui/table/dialog/template/TemplatesWidget.cpp index 897fd44d..cd1ff485 100644 --- a/src/ui/table/dialog/template/TemplatesWidget.cpp +++ b/src/ui/table/dialog/template/TemplatesWidget.cpp @@ -6,6 +6,7 @@ #include #include +#include "UtilsFiles.hpp" #include "UtilsGeneral.hpp" TemplatesWidget::TemplatesWidget(QWidget* parent) : @@ -98,7 +99,8 @@ TemplatesWidget::removeButtonClicked() Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("Could not remove Character!")); return; } - if (!m_charFileHandler->removeCharacter(character.name + ".char")) { + if (const auto charRemoved = Utils::Files::removeFile(m_charFileHandler->getDirectoryString() + character.name + ".char"); + !charRemoved) { Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("The Character could not be removed!")); } } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index bca92a38..c4dbcd83 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -5,6 +5,8 @@ target_include_directories (utils ) target_sources(utils INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/UtilsFiles.cpp + ${CMAKE_CURRENT_LIST_DIR}/UtilsFiles.hpp ${CMAKE_CURRENT_LIST_DIR}/UtilsGeneral.cpp ${CMAKE_CURRENT_LIST_DIR}/UtilsGeneral.hpp ${CMAKE_CURRENT_LIST_DIR}/UtilsTable.cpp diff --git a/src/utils/UtilsFiles.cpp b/src/utils/UtilsFiles.cpp new file mode 100644 index 00000000..d9a149f8 --- /dev/null +++ b/src/utils/UtilsFiles.cpp @@ -0,0 +1,16 @@ +#include "UtilsFiles.hpp" + +#include + +namespace Utils::Files +{ +bool +removeFile(const QString& fileName) +{ + QFile fileOut(fileName); + if (!fileOut.exists()) { + return false; + } + return fileOut.remove(); +}; +} diff --git a/src/utils/UtilsFiles.hpp b/src/utils/UtilsFiles.hpp new file mode 100644 index 00000000..04da47a0 --- /dev/null +++ b/src/utils/UtilsFiles.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +// Utils for file handling +namespace Utils::Files +{ +// Remove a file +[[nodiscard]] bool +removeFile(const QString& fileName); +} diff --git a/test/handler/CharFileHandlerTest.cpp b/test/handler/CharFileHandlerTest.cpp index 3b5833c2..6ca0a1d0 100644 --- a/test/handler/CharFileHandlerTest.cpp +++ b/test/handler/CharFileHandlerTest.cpp @@ -9,8 +9,6 @@ #endif #include -#include -#include TEST_CASE("CharFileHandler Testing", "[CharFileHandler]") { auto const charFileHandler = std::make_shared(); @@ -41,12 +39,8 @@ TEST_CASE("CharFileHandler Testing", "[CharFileHandler]") { // Incomplete json object QJsonObject jsonObject; jsonObject["name"] = 2; - // Write to file - auto byteArray = QJsonDocument(jsonObject).toJson(); - QFile fileOut(dir.currentPath() + "/chars/broken.char"); - fileOut.open(QIODevice::WriteOnly); - fileOut.write(byteArray); + charFileHandler->writeJsonObjectToFile(jsonObject, dir.currentPath() + "/chars/broken.char"); REQUIRE(charFileHandler->getStatus("broken.char") == 1); dir.remove(dir.currentPath() + "/chars/broken.char"); @@ -55,9 +49,6 @@ TEST_CASE("CharFileHandler Testing", "[CharFileHandler]") { const auto codeCSVStatus = charFileHandler->getStatus("nonexisting.char"); REQUIRE(codeCSVStatus == 2); } - SECTION("Check file removal") { - const auto fileRemoved = charFileHandler->removeCharacter("test.char"); - REQUIRE(fileRemoved == true); - REQUIRE(!dir.exists(charPath)); - } + + std::remove("./chars/test.char"); } diff --git a/test/handler/EffectFileHandlerTest.cpp b/test/handler/EffectFileHandlerTest.cpp index 0d2d9e4e..3e0d7b62 100644 --- a/test/handler/EffectFileHandlerTest.cpp +++ b/test/handler/EffectFileHandlerTest.cpp @@ -7,8 +7,6 @@ #endif #include -#include -#include TEST_CASE("EffectFileHandler Testing", "[EffectFileHandler]") { auto const effectFileHandler = std::make_unique(); @@ -32,13 +30,9 @@ TEST_CASE("EffectFileHandler Testing", "[EffectFileHandler]") { SECTION("Broken table") { // Incomplete json object QJsonObject jsonObject; - jsonObject["name"] = 2; - + jsonObject["broken"] = 2; // Write to file - auto byteArray = QJsonDocument(jsonObject).toJson(); - QFile fileOut(dir.currentPath() + "/effects/Broken Effect.effect"); - fileOut.open(QIODevice::WriteOnly); - fileOut.write(byteArray); + effectFileHandler->writeJsonObjectToFile(jsonObject, dir.currentPath() + "/effects/Broken Effect.effect"); REQUIRE(effectFileHandler->getStatus("Broken Effect.effect") == 1); dir.remove(dir.currentPath() + "/effects/Broken Effect.effect"); @@ -47,9 +41,6 @@ TEST_CASE("EffectFileHandler Testing", "[EffectFileHandler]") { const auto codeCSVStatus = effectFileHandler->getStatus("nonexisting.effect"); REQUIRE(codeCSVStatus == 2); } - SECTION("Check file removal") { - const auto fileRemoved = effectFileHandler->removeEffect("Test Effect.effect"); - REQUIRE(fileRemoved == true); - REQUIRE(!dir.exists(effectPath)); - } + + std::remove("./effects/Test Effect.effect"); } diff --git a/test/handler/TableFileHandlerTest.cpp b/test/handler/TableFileHandlerTest.cpp index 54144818..d132d1e6 100644 --- a/test/handler/TableFileHandlerTest.cpp +++ b/test/handler/TableFileHandlerTest.cpp @@ -11,9 +11,7 @@ #include #endif -#include #include -#include #include #include @@ -151,12 +149,8 @@ TEST_CASE_METHOD(FileHandlerTestUtils, "TableFileHandler Testing", "[TableFileHa QJsonObject jsonObject; jsonObject["row_entered"] = 2; jsonObject["round_counter"] = 3; - // Write to file - auto byteArray = QJsonDocument(jsonObject).toJson(); - QFile fileOut("./broken.lcm"); - fileOut.open(QIODevice::WriteOnly); - fileOut.write(byteArray); + tableFileHandler->writeJsonObjectToFile(jsonObject, "./broken.lcm"); REQUIRE(tableFileHandler->getStatus(resolvePath("./broken.lcm")) == 1); } diff --git a/test/utils/GeneralUtilsTest.cpp b/test/utils/GeneralUtilsTest.cpp index be852de4..f4c25b45 100644 --- a/test/utils/GeneralUtilsTest.cpp +++ b/test/utils/GeneralUtilsTest.cpp @@ -1,4 +1,3 @@ -#include "AdditionalInfoWidget.hpp" #include "UtilsGeneral.hpp" #ifdef CATCH2_V3 @@ -7,8 +6,6 @@ #include #endif -#include - TEST_CASE("General Util Testing", "[GeneralUtils]") { SECTION("CSV file path test") { SECTION("Example Latin") { diff --git a/test/utils/UtilsFilesTest.cpp b/test/utils/UtilsFilesTest.cpp new file mode 100644 index 00000000..2735215f --- /dev/null +++ b/test/utils/UtilsFilesTest.cpp @@ -0,0 +1,23 @@ +#include "UtilsFiles.hpp" + +#ifdef CATCH2_V3 +#include +#else +#include +#endif + +#include + +TEST_CASE("Utils Files Testing", "[UtilsFiles]") { + SECTION("Remove file test") { + std::ofstream file; + file.open("existing_file.txt"); + file.close(); + + auto fileRemoved = Utils::Files::removeFile("existing_file.txt"); + REQUIRE(fileRemoved == false); + + fileRemoved = Utils::Files::removeFile("nonexisting_file.txt"); + REQUIRE(fileRemoved == false); + } +} From 16848d8db04e6dfb89fd06f6db996390340aaf3d Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 15:38:26 +0100 Subject: [PATCH 03/22] [redesign/fix] Delete chars/effects now based on file name content --- src/ui/table/dialog/StatusEffectDialog.cpp | 9 ++++-- .../dialog/template/TemplatesListWidget.cpp | 8 ++--- .../dialog/template/TemplatesListWidget.hpp | 4 +-- .../table/dialog/template/TemplatesWidget.cpp | 15 +++++---- src/utils/UtilsFiles.cpp | 31 +++++++++++++++++-- src/utils/UtilsFiles.hpp | 8 +++++ test/ui/widget/TemplatesListWidgetTest.cpp | 8 +---- test/utils/UtilsFilesTest.cpp | 16 +++++++++- 8 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/ui/table/dialog/StatusEffectDialog.cpp b/src/ui/table/dialog/StatusEffectDialog.cpp index 92777a9a..ef82420f 100644 --- a/src/ui/table/dialog/StatusEffectDialog.cpp +++ b/src/ui/table/dialog/StatusEffectDialog.cpp @@ -170,8 +170,13 @@ StatusEffectDialog::removeEffectButtonClicked() return; } - if (const auto effectRemoved = Utils::Files::removeFile(m_effectFileHandler->getDirectoryString() + selectedItems.at(0)->text(COL_NAME) + ".effect"); - !effectRemoved) { + const auto foundEffect = Utils::Files::findObject(m_effectFileHandler->getDirectoryString(), "*.effect", selectedItems.at(0)->text(COL_NAME)); + if (!foundEffect.has_value()) { + Utils::General::displayWarningMessageBox(this, tr("Effect not found!"), tr("The Effect was not found on disc!")); + return; + } + + if (const auto effectRemoved = Utils::Files::removeFile(foundEffect.value()); !effectRemoved) { Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("Could not remove Effect!")); return; } diff --git a/src/ui/table/dialog/template/TemplatesListWidget.cpp b/src/ui/table/dialog/template/TemplatesListWidget.cpp index e3dc8694..a1c4e0d5 100644 --- a/src/ui/table/dialog/template/TemplatesListWidget.cpp +++ b/src/ui/table/dialog/template/TemplatesListWidget.cpp @@ -28,16 +28,14 @@ TemplatesListWidget::addCharacter(const CharacterHandler::Character& character) } -bool -TemplatesListWidget::removeCharacter(const CharacterHandler::Character &character) +void +TemplatesListWidget::removeCharacter(const QString& characterName) { for (auto i = 0; i < count(); i++) { auto const storedCharacter = item(i)->data(Qt::UserRole).value(); - if (storedCharacter.name == character.name) { + if (storedCharacter.name == characterName) { QListWidgetItem *it = this->takeItem(i); delete it; - return true; } } - return false; } diff --git a/src/ui/table/dialog/template/TemplatesListWidget.hpp b/src/ui/table/dialog/template/TemplatesListWidget.hpp index aa970664..740271bf 100644 --- a/src/ui/table/dialog/template/TemplatesListWidget.hpp +++ b/src/ui/table/dialog/template/TemplatesListWidget.hpp @@ -15,6 +15,6 @@ class TemplatesListWidget : public QListWidget { bool addCharacter(const CharacterHandler::Character& character); - bool - removeCharacter(const CharacterHandler::Character& character); + void + removeCharacter(const QString& characterName); }; diff --git a/src/ui/table/dialog/template/TemplatesWidget.cpp b/src/ui/table/dialog/template/TemplatesWidget.cpp index cd1ff485..a90a78cb 100644 --- a/src/ui/table/dialog/template/TemplatesWidget.cpp +++ b/src/ui/table/dialog/template/TemplatesWidget.cpp @@ -94,13 +94,16 @@ TemplatesWidget::removeButtonClicked() return; } - const auto character = m_templatesListWidget->selectedItems().first()->data(Qt::UserRole).value(); - if (!m_templatesListWidget->removeCharacter(character)) { - Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("Could not remove Character!")); + const auto characterName = m_templatesListWidget->selectedItems().first()->data(Qt::UserRole).value().name; + const auto foundCharacter = Utils::Files::findObject(m_charFileHandler->getDirectoryString(), "*.char", characterName); + if (!foundCharacter.has_value()) { + Utils::General::displayWarningMessageBox(this, tr("Character not found!"), tr("The Character was not found on disc!")); return; } - if (const auto charRemoved = Utils::Files::removeFile(m_charFileHandler->getDirectoryString() + character.name + ".char"); - !charRemoved) { - Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("The Character could not be removed!")); + + if (const auto charRemoved = Utils::Files::removeFile(foundCharacter.value()); !charRemoved) { + Utils::General::displayWarningMessageBox(this, tr("Action not possible!"), tr("The Character could not be removed from disc!")); + return; } + m_templatesListWidget->removeCharacter(characterName); } diff --git a/src/utils/UtilsFiles.cpp b/src/utils/UtilsFiles.cpp index d9a149f8..d775d75f 100644 --- a/src/utils/UtilsFiles.cpp +++ b/src/utils/UtilsFiles.cpp @@ -1,16 +1,41 @@ #include "UtilsFiles.hpp" +#include #include +#include +#include namespace Utils::Files { bool removeFile(const QString& fileName) { - QFile fileOut(fileName); - if (!fileOut.exists()) { + QFile file(fileName); + if (!file.exists()) { return false; } - return fileOut.remove(); + + return file.remove(); }; + + +std::optional +findObject(const QString& directory, const QString& fileEnding, const QString& objectName) +{ + QDirIterator it(directory, { fileEnding }, QDir::Files); + while (it.hasNext()) { + it.next(); + + QFile file(it.filePath()); + file.open(QIODevice::ReadOnly); + const auto jsonObject = QJsonDocument::fromJson(file.readAll()).object(); + file.close(); + + if (jsonObject["name"] == objectName) { + return it.filePath(); + } + } + + return {}; +} } diff --git a/src/utils/UtilsFiles.hpp b/src/utils/UtilsFiles.hpp index 04da47a0..7a6a1524 100644 --- a/src/utils/UtilsFiles.hpp +++ b/src/utils/UtilsFiles.hpp @@ -2,10 +2,18 @@ #include +#include + // Utils for file handling namespace Utils::Files { // Remove a file [[nodiscard]] bool removeFile(const QString& fileName); + +// Find a json object inside a directory by a certain name +std::optional +findObject(const QString& directory, + const QString& fileEnding, + const QString& objectName); } diff --git a/test/ui/widget/TemplatesListWidgetTest.cpp b/test/ui/widget/TemplatesListWidgetTest.cpp index 354fc344..b6bd07ab 100644 --- a/test/ui/widget/TemplatesListWidgetTest.cpp +++ b/test/ui/widget/TemplatesListWidgetTest.cpp @@ -32,13 +32,7 @@ TEST_CASE("Templates List Widget Testing", "[TableUtils]") { REQUIRE(sameCharacterAddedAgain == false); } SECTION("Remove character test") { - const auto characterRemoved = templatesListWidget->removeCharacter(character); - REQUIRE(characterRemoved == true); + templatesListWidget->removeCharacter(character.name); REQUIRE(templatesListWidget->count() == 0); } - SECTION("Remove another unadded character test") { - const auto anotherCharacter = CharacterHandler::Character("test2", 0, -3, 10, false, AdditionalInfoData{ .mainInfoText = "Haste" }); - const auto characterRemoved = templatesListWidget->removeCharacter(anotherCharacter); - REQUIRE(characterRemoved == false); - } } diff --git a/test/utils/UtilsFilesTest.cpp b/test/utils/UtilsFilesTest.cpp index 2735215f..958bbce3 100644 --- a/test/utils/UtilsFilesTest.cpp +++ b/test/utils/UtilsFilesTest.cpp @@ -15,9 +15,23 @@ TEST_CASE("Utils Files Testing", "[UtilsFiles]") { file.close(); auto fileRemoved = Utils::Files::removeFile("existing_file.txt"); - REQUIRE(fileRemoved == false); + REQUIRE(fileRemoved == true); fileRemoved = Utils::Files::removeFile("nonexisting_file.txt"); REQUIRE(fileRemoved == false); } + SECTION("Find object test") { + std::ofstream file; + file.open("test.file"); + file << "{\"name\": \"a_random_name\"}"; + file.close(); + + auto foundEffect = Utils::Files::findObject(".", "*.file", "a_random_name"); + REQUIRE(foundEffect.has_value()); + + foundEffect = Utils::Files::findObject("", "*.txt", "a_random_name"); + REQUIRE(!foundEffect.has_value()); + foundEffect = Utils::Files::findObject("", "*.file", "another_name"); + REQUIRE(!foundEffect.has_value()); + } } From 90d1df5fa5427a6748b8a7d6c75bab9c50d1969e Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 15:38:45 +0100 Subject: [PATCH 04/22] [infrastructure] Add missing link to file utils tests --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d1b4973d..cedf89d7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable(tests ${CMAKE_CURRENT_LIST_DIR}/ui/widget/TemplatesListWidgetTest.cpp ${CMAKE_CURRENT_LIST_DIR}/utils/GeneralUtilsTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/utils/UtilsFilesTest.cpp ) target_link_libraries(tests From e2c75b6b105085ae2a37894a44268fb74f52b6cf Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 15:42:39 +0100 Subject: [PATCH 05/22] [redesign] Readjust icon ordering for toolbar and context menu --- src/ui/table/CombatWidget.cpp | 19 +++++++++++-------- src/ui/table/CombatWidget.hpp | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index 20d0ac47..39894a2a 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -44,11 +44,11 @@ CombatWidget::CombatWidget(std::shared_ptr tableFilerHandler, m_undoStack = new QUndoStack(this); + m_removeCharacterAction = createAction(tr("Remove"), tr("Remove Character(s)"), QKeySequence(Qt::Key_Delete), false); m_addCharacterAction = createAction(tr("Add new Character(s)..."), tr("Add new Character(s)"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_N), true); m_insertTableAction = createAction(tr("Insert other Table..."), tr("Insert another table without overwriting the current one"), QKeySequence(tr("Ctrl+T")), false); - m_removeAction = createAction(tr("Remove"), tr("Remove Character(s)"), QKeySequence(Qt::Key_Delete), false); m_addEffectAction = createAction(tr("Add Status Effect(s)..."), tr("Add Status Effect(s)"), QKeySequence(tr("Ctrl+E")), false); m_duplicateAction = createAction(tr("Duplicate"), tr("Duplicate Character"), QKeySequence(tr("Ctrl+D")), false); m_rerollAction = createAction(tr("Reroll Initiative"), tr("Reroll Initiative"), QKeySequence(tr("Ctrl+I")), false); @@ -75,9 +75,10 @@ CombatWidget::CombatWidget(std::shared_ptr tableFilerHandler, setUndoRedoIcon(isSystemInDarkMode); auto* const toolBar = new QToolBar("Actions"); + toolBar->addAction(m_removeCharacterAction); toolBar->addAction(m_addCharacterAction); + toolBar->addSeparator(); toolBar->addAction(m_insertTableAction); - toolBar->addAction(m_removeAction); toolBar->addAction(m_addEffectAction); toolBar->addAction(m_resortAction); toolBar->addSeparator(); @@ -152,9 +153,9 @@ CombatWidget::CombatWidget(std::shared_ptr tableFilerHandler, m_roundCounterLabel->setText(tr("Round ") + QString::number(m_roundCounter)); }); + connect(m_removeCharacterAction, &QAction::triggered, this, &CombatWidget::removeRow); connect(m_addCharacterAction, &QAction::triggered, this, &CombatWidget::openAddCharacterDialog); connect(m_insertTableAction, &QAction::triggered, this, &CombatWidget::insertTable); - connect(m_removeAction, &QAction::triggered, this, &CombatWidget::removeRow); connect(m_addEffectAction, &QAction::triggered, this, &CombatWidget::openStatusEffectDialog); connect(m_duplicateAction, &QAction::triggered, this, &CombatWidget::duplicateRow); connect(m_rerollAction, &QAction::triggered, this, &CombatWidget::rerollIni); @@ -186,7 +187,7 @@ CombatWidget::CombatWidget(std::shared_ptr tableFilerHandler, saveOldState(); }); connect(m_tableWidget, &QTableWidget::itemSelectionChanged, this, [this] { - m_removeAction->setEnabled(m_tableWidget->selectionModel()->hasSelection()); + m_removeCharacterAction->setEnabled(m_tableWidget->selectionModel()->hasSelection()); m_addEffectAction->setEnabled(m_tableWidget->selectionModel()->hasSelection()); m_duplicateAction->setEnabled(m_tableWidget->selectionModel()->selectedRows().size() == 1); m_rerollAction->setEnabled(m_tableWidget->selectionModel()->selectedRows().size() == 1); @@ -323,9 +324,9 @@ CombatWidget::resetNameAndInfoWidth(const int nameWidth, const int addInfoWidth) void CombatWidget::setUndoRedoIcon(bool isDarkMode) { + m_removeCharacterAction->setIcon(isDarkMode ? QIcon(":/icons/table/remove_white.svg") : QIcon(":/icons/table/remove_black.svg")); m_addCharacterAction->setIcon(isDarkMode ? QIcon(":/icons/table/add_white.svg") : QIcon(":/icons/table/add_black.svg")); m_insertTableAction->setIcon(isDarkMode ? QIcon(":/icons/table/insert_table_white.svg") : QIcon(":/icons/table/insert_table_black.svg")); - m_removeAction->setIcon(isDarkMode ? QIcon(":/icons/table/remove_white.svg") : QIcon(":/icons/table/remove_black.svg")); m_addEffectAction->setIcon(isDarkMode ? QIcon(":/icons/table/effect_white.svg") : QIcon(":/icons/table/effect_black.svg")); m_duplicateAction->setIcon(isDarkMode ? QIcon(":/icons/table/duplicate_white.svg") : QIcon(":/icons/table/duplicate_black.svg")); m_rerollAction->setIcon(isDarkMode ? QIcon(":/icons/table/reroll_white.svg") : QIcon(":/icons/table/reroll_black.svg")); @@ -860,15 +861,17 @@ CombatWidget::contextMenuEvent(QContextMenuEvent *event) { auto *const menu = new QMenu(this); + const auto currentRow = m_tableWidget->indexAt(m_tableWidget->viewport()->mapFrom(this, event->pos())).row(); + if (currentRow >= 0) { + menu->addAction(m_removeCharacterAction); + } menu->addAction(m_addCharacterAction); + if (m_tableWidget->rowCount() > 0) { menu->addAction(m_insertTableAction); } - - const auto currentRow = m_tableWidget->indexAt(m_tableWidget->viewport()->mapFrom(this, event->pos())).row(); // Map from MainWindow coordinates to Table Widget coordinates if (currentRow >= 0) { - menu->addAction(m_removeAction); menu->addAction(m_addEffectAction); if (m_tableWidget->rowCount() > 1) { diff --git a/src/ui/table/CombatWidget.hpp b/src/ui/table/CombatWidget.hpp index 1e9bb05e..7294d185 100644 --- a/src/ui/table/CombatWidget.hpp +++ b/src/ui/table/CombatWidget.hpp @@ -176,9 +176,9 @@ private slots: QPointer m_timer; + QPointer m_removeCharacterAction; QPointer m_addCharacterAction; QPointer m_insertTableAction; - QPointer m_removeAction; QPointer m_addEffectAction; QPointer m_duplicateAction; QPointer m_rerollAction; From 6da34893b13fdd8918900d304ce7827efff27616 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 15:45:01 +0100 Subject: [PATCH 06/22] [infrastructure] Rename general utils test --- test/CMakeLists.txt | 2 +- test/utils/{GeneralUtilsTest.cpp => UtilsGeneralTest.cpp} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename test/utils/{GeneralUtilsTest.cpp => UtilsGeneralTest.cpp} (92%) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cedf89d7..d864acf4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,7 +21,7 @@ add_executable(tests ${CMAKE_CURRENT_LIST_DIR}/ui/widget/LogListWidgetTest.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/widget/TemplatesListWidgetTest.cpp - ${CMAKE_CURRENT_LIST_DIR}/utils/GeneralUtilsTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/utils/UtilsGeneralTest.cpp ${CMAKE_CURRENT_LIST_DIR}/utils/UtilsFilesTest.cpp ) diff --git a/test/utils/GeneralUtilsTest.cpp b/test/utils/UtilsGeneralTest.cpp similarity index 92% rename from test/utils/GeneralUtilsTest.cpp rename to test/utils/UtilsGeneralTest.cpp index f4c25b45..0d9113ec 100644 --- a/test/utils/GeneralUtilsTest.cpp +++ b/test/utils/UtilsGeneralTest.cpp @@ -6,7 +6,7 @@ #include #endif -TEST_CASE("General Util Testing", "[GeneralUtils]") { +TEST_CASE("Utils General Testing", "[UtilsGeneral]") { SECTION("CSV file path test") { SECTION("Example Latin") { REQUIRE(Utils::General::getLCMName("a/path/to/an/exampleTable.csv") == "exampleTable.csv"); From 31b153282a8ce92c68e1854b9ed416615fec6c5f Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 18:17:29 +0100 Subject: [PATCH 07/22] [feature] Option to readjust table height after character is removed --- src/ui/MainWindow.cpp | 2 +- src/ui/settings/TableSettings.cpp | 4 ++++ src/ui/settings/TableSettings.hpp | 10 ++++++---- src/ui/table/CombatWidget.cpp | 11 +++++++++++ test/ui/settings/SettingsTest.cpp | 5 +++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 739e72d3..b1d1a8f6 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -268,7 +268,7 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) setCentralWidget(m_combatWidget); connect(m_combatWidget, &CombatWidget::exit, this, &MainWindow::exitCombat); connect(m_combatWidget, &CombatWidget::tableHeightSet, this, [this] (unsigned int height) { - if (height > START_HEIGHT && (int) height > this->height()) { + if (height > START_HEIGHT) { resize(width(), height); } }); diff --git a/src/ui/settings/TableSettings.cpp b/src/ui/settings/TableSettings.cpp index fb638a3e..e23305bb 100644 --- a/src/ui/settings/TableSettings.cpp +++ b/src/ui/settings/TableSettings.cpp @@ -27,6 +27,9 @@ TableSettings::write(ValueType valueType, bool valueToWrite) break; case SHOW_INI_TOOLTIPS: writeParameter(settings, valueToWrite, showIniToolTips, "ini_tool_tips"); + break; + case ADJUST_HEIGHT_AFTER_REMOVE: + writeParameter(settings, valueToWrite, adjustHeightAfterRemove, "adjust_height_remove"); default: break; } @@ -45,5 +48,6 @@ TableSettings::read() modifierShown = settings.value("modifier").isValid() ? settings.value("modifier").toBool() : true; colorTableRows = settings.value("color_rows").isValid() ? settings.value("color_rows").toBool() : false; showIniToolTips = settings.value("ini_tool_tips").isValid() ? settings.value("ini_tool_tips").toBool() : false; + adjustHeightAfterRemove = settings.value("adjust_height_remove").isValid() ? settings.value("adjust_height_remove").toBool() : false; settings.endGroup(); } diff --git a/src/ui/settings/TableSettings.hpp b/src/ui/settings/TableSettings.hpp index 7043a8db..20fc2ea1 100644 --- a/src/ui/settings/TableSettings.hpp +++ b/src/ui/settings/TableSettings.hpp @@ -8,10 +8,11 @@ class TableSettings : public BaseSettings { TableSettings(); enum class ValueType { - INI_SHOWN = 0, - MOD_SHOWN = 1, - COLOR_TABLE = 2, - SHOW_INI_TOOLTIPS = 3 + INI_SHOWN = 0, + MOD_SHOWN = 1, + COLOR_TABLE = 2, + SHOW_INI_TOOLTIPS = 3, + ADJUST_HEIGHT_AFTER_REMOVE = 4 }; void @@ -23,6 +24,7 @@ class TableSettings : public BaseSettings { bool modifierShown{ true }; bool colorTableRows{ false }; bool showIniToolTips{ false }; + bool adjustHeightAfterRemove{ false }; private: void diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index 39894a2a..d4f75add 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -616,6 +616,10 @@ CombatWidget::removeRow() setRowAndPlayer(); pushOnUndoStack(); m_tableWidget->itemSelectionChanged(); + + if (m_tableSettings.adjustHeightAfterRemove) { + emit tableHeightSet(m_tableWidget->getHeight() + 40); + } } @@ -801,6 +805,7 @@ CombatWidget::setTableOption(bool option, int valueType) case 3: m_tableWidget->setIniColumnTooltips(!option); break; + case 4: default: break; } @@ -925,5 +930,11 @@ CombatWidget::contextMenuEvent(QContextMenuEvent *event) showIniTooltipsAction->setCheckable(true); showIniTooltipsAction->setChecked(m_tableSettings.showIniToolTips); + auto *const adjustHeightAfterRemoveAction = optionMenu->addAction(tr("Readjust Height after Character Removal"), this, [this] (bool show) { + setTableOption(show, 4); + }); + adjustHeightAfterRemoveAction->setCheckable(true); + adjustHeightAfterRemoveAction->setChecked(m_tableSettings.adjustHeightAfterRemove); + menu->exec(event->globalPos()); } diff --git a/test/ui/settings/SettingsTest.cpp b/test/ui/settings/SettingsTest.cpp index a3070110..fa0a3625 100644 --- a/test/ui/settings/SettingsTest.cpp +++ b/test/ui/settings/SettingsTest.cpp @@ -102,23 +102,28 @@ TEST_CASE("Settings Testing", "[Settings]") { REQUIRE(settings.value("modifier").isValid() == false); REQUIRE(settings.value("color_rows").isValid() == false); REQUIRE(settings.value("ini_tool_tips").isValid() == false); + REQUIRE(settings.value("adjust_height_remove").isValid() == false); settings.endGroup(); tableSettings.write(TableSettings::ValueType::INI_SHOWN, false); tableSettings.write(TableSettings::ValueType::MOD_SHOWN, false); tableSettings.write(TableSettings::ValueType::COLOR_TABLE, true); tableSettings.write(TableSettings::ValueType::SHOW_INI_TOOLTIPS, true); + tableSettings.write(TableSettings::ValueType::ADJUST_HEIGHT_AFTER_REMOVE, true); settings.beginGroup("table"); REQUIRE(settings.value("ini").isValid() == true); REQUIRE(settings.value("modifier").isValid() == true); REQUIRE(settings.value("color_rows").isValid() == true); REQUIRE(settings.value("ini_tool_tips").isValid() == true); + REQUIRE(settings.value("adjust_height_remove").isValid() == true); REQUIRE(settings.value("ini").toBool() == false); REQUIRE(settings.value("modifier").toBool() == false); REQUIRE(settings.value("color_rows").toBool() == true); REQUIRE(settings.value("ini_tool_tips").toBool() == true); + REQUIRE(settings.value("adjust_height_remove").toBool() == true); settings.endGroup(); + settings.clear(); } } From 12fa7b8ea062a3c41136851114cfecc90b81ee25 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Mon, 23 Dec 2024 23:23:51 +0100 Subject: [PATCH 08/22] [fix] Height setting was buggy when calling redo operations --- src/ui/MainWindow.cpp | 5 +++-- src/ui/table/CombatTableWidget.cpp | 2 +- src/ui/table/CombatTableWidget.hpp | 2 +- src/ui/table/CombatWidget.cpp | 6 +++--- src/ui/table/Undo.cpp | 2 +- src/utils/UtilsTable.hpp | 2 ++ 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index b1d1a8f6..c30ce398 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -268,9 +268,10 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) setCentralWidget(m_combatWidget); connect(m_combatWidget, &CombatWidget::exit, this, &MainWindow::exitCombat); connect(m_combatWidget, &CombatWidget::tableHeightSet, this, [this] (unsigned int height) { - if (height > START_HEIGHT) { - resize(width(), height); + if (height <= START_HEIGHT) { + return; } + resize(width(), height); }); connect(m_combatWidget, &CombatWidget::tableWidthSet, this, [this] (int tableWidth) { // @note A single immediate call to resize() won't actually resize the window diff --git a/src/ui/table/CombatTableWidget.cpp b/src/ui/table/CombatTableWidget.cpp index 4a478ee6..6aefa7aa 100644 --- a/src/ui/table/CombatTableWidget.cpp +++ b/src/ui/table/CombatTableWidget.cpp @@ -216,7 +216,7 @@ CombatTableWidget::getHeight() const for (int i = 0; i < rowCount(); i++) { height += rowHeight(i); } - return height + HEIGHT_BUFFER; + return height + TABLE_HEIGHT_BUFFER; } diff --git a/src/ui/table/CombatTableWidget.hpp b/src/ui/table/CombatTableWidget.hpp index 9dde0ddf..c107b796 100644 --- a/src/ui/table/CombatTableWidget.hpp +++ b/src/ui/table/CombatTableWidget.hpp @@ -67,7 +67,7 @@ class CombatTableWidget : public QTableWidget { static constexpr int FIRST_FIVE_COLUMNS = 5; static constexpr int NMBR_COLUMNS = 6; - static constexpr int HEIGHT_BUFFER = 140; + static constexpr int TABLE_HEIGHT_BUFFER = 140; static constexpr float WIDTH_NAME = 0.20f; static constexpr float WIDTH_INI = 0.05f; diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index d4f75add..417d8bc8 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -349,7 +349,7 @@ CombatWidget::openAddCharacterDialog() auto *const dialog = new AddCharacterDialog(m_additionalSettings.modAddedToIni, this); connect(dialog, &AddCharacterDialog::characterCreated, this, [this, &sizeBeforeDialog] (CharacterHandler::Character character, int instanceCount) { addCharacter(character, instanceCount); - emit tableHeightSet(m_tableWidget->getHeight() + 40); + emit tableHeightSet(m_tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); m_logListWidget->logConditionalValue(COUNT, m_characterHandler->getCharacters().size() - sizeBeforeDialog, true); sizeBeforeDialog = m_characterHandler->getCharacters().size(); @@ -416,7 +416,7 @@ CombatWidget::insertTable() std::iota(m_affectedRowIndices.begin(), m_affectedRowIndices.end(), oldSize); pushOnUndoStack(); - emit tableHeightSet(m_tableWidget->getHeight() + 40); + emit tableHeightSet(m_tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); auto const reply = QMessageBox::question(this, tr("Sort Table?"), tr("Do you want to resort the Table?"), QMessageBox::Yes | QMessageBox::No); @@ -618,7 +618,7 @@ CombatWidget::removeRow() m_tableWidget->itemSelectionChanged(); if (m_tableSettings.adjustHeightAfterRemove) { - emit tableHeightSet(m_tableWidget->getHeight() + 40); + emit tableHeightSet(m_tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); } } diff --git a/src/ui/table/Undo.cpp b/src/ui/table/Undo.cpp index 1f547a7f..4c2fc276 100644 --- a/src/ui/table/Undo.cpp +++ b/src/ui/table/Undo.cpp @@ -83,7 +83,7 @@ Undo::setCombatWidget(bool undo) tableWidget->setTableRowColor(!m_colorTableRows); tableWidget->setIniColumnTooltips(!m_showIniToolTips); - emit m_combatWidget->tableHeightSet(tableWidget->getHeight()); + emit m_combatWidget->tableHeightSet(tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); emit m_combatWidget->changeOccured(); tableWidget->blockSignals(false); diff --git a/src/utils/UtilsTable.hpp b/src/utils/UtilsTable.hpp index f4b5411e..034fb46e 100644 --- a/src/utils/UtilsTable.hpp +++ b/src/utils/UtilsTable.hpp @@ -14,6 +14,8 @@ setTableAdditionalInfoWidget(CombatWidget* combatWidget, unsigned int row, const QVariant& additionalInfo); +static constexpr int HEIGHT_BUFFER = 40; + static constexpr int COL_NAME = 0; static constexpr int COL_INI = 1; static constexpr int COL_MODIFIER = 2; From 99b801abd418fd2e48c00c3fad9e18e1d72cf4e3 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Thu, 16 Jan 2025 20:16:40 +0100 Subject: [PATCH 09/22] [fix] Did not ask to sort correctly --- src/ui/table/CombatWidget.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index 417d8bc8..099277ff 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -344,7 +344,7 @@ CombatWidget::openAddCharacterDialog() { // Resynchronize because the table could have been modified m_tableWidget->resynchronizeCharacters(); - auto sizeBeforeDialog = m_characterHandler->getCharacters().size(); + const auto sizeBeforeDialog = m_characterHandler->getCharacters().size(); auto *const dialog = new AddCharacterDialog(m_additionalSettings.modAddedToIni, this); connect(dialog, &AddCharacterDialog::characterCreated, this, [this, &sizeBeforeDialog] (CharacterHandler::Character character, int instanceCount) { @@ -352,7 +352,6 @@ CombatWidget::openAddCharacterDialog() emit tableHeightSet(m_tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); m_logListWidget->logConditionalValue(COUNT, m_characterHandler->getCharacters().size() - sizeBeforeDialog, true); - sizeBeforeDialog = m_characterHandler->getCharacters().size(); }); if (dialog->exec() == QDialog::Accepted) { From e812255edc3c13de3a6f74f472278c0f6dc0710d Mon Sep 17 00:00:00 2001 From: Bakefish Date: Thu, 16 Jan 2025 20:53:42 +0100 Subject: [PATCH 10/22] [refactor] Redesign messagebox for different ruleset loaded --- src/ui/MainWindow.cpp | 16 ++++++++-------- src/utils/UtilsGeneral.cpp | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index c30ce398..11fb951c 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -174,8 +174,8 @@ MainWindow::openTable() if (!checkStoredTableRules(m_tableFileHandler->getData())) { const auto messageString = createRuleChangeMessageBoxText(); auto *const msgBox = new QMessageBox(QMessageBox::Warning, tr("Different Rulesets detected!"), messageString, QMessageBox::Cancel); - auto* const applyButton = msgBox->addButton(tr("Apply Table Ruleset to Settings"), QMessageBox::ApplyRole); - auto* const ignoreButton = msgBox->addButton(tr("Ignore stored Table Ruleset"), QMessageBox::AcceptRole); + auto* const ignoreButton = msgBox->addButton(tr("Use Settings Ruleset"), QMessageBox::AcceptRole); + auto* const applyButton = msgBox->addButton(tr("Use Table Ruleset (Apply to Settings)"), QMessageBox::ApplyRole); msgBox->exec(); if (msgBox->clickedButton() == applyButton) { @@ -360,12 +360,12 @@ MainWindow::createSaveMessageBox(const QString& tableMessage, bool isClosing) QString MainWindow::createRuleChangeMessageBoxText() const { - const auto message = tr("The Table you are trying to load uses another ruleset than you have stored in your rule settings!

" - "Your ruleset: ") + Utils::General::getRulesetName(m_ruleSettings.ruleset) + ", " + - "" + Utils::General::getAutoRollEnabled(m_ruleSettings.rollAutomatical) + "
" + - tr("The stored table ruleset is: ") + Utils::General::getRulesetName(m_loadedTableRule) + ", " + - "" + Utils::General::getAutoRollEnabled(m_loadedTableRollAutomatically) + "

" + - tr("Do you want to apply the stored Table ruleset to your settings or ignore it?"); + const auto message = tr("The loaded table uses a different ruleset from your settings!

" + "Settings ruleset: ") + Utils::General::getRulesetName(m_ruleSettings.ruleset) + ", " + + Utils::General::getAutoRollEnabled(m_ruleSettings.rollAutomatical) + "
" + + tr("Table ruleset: ") + Utils::General::getRulesetName(m_loadedTableRule) + ", " + + Utils::General::getAutoRollEnabled(m_loadedTableRollAutomatically) + "

" + + tr("Do you want to use your settings or the table ruleset?"); return message; } diff --git a/src/utils/UtilsGeneral.cpp b/src/utils/UtilsGeneral.cpp index a4085129..ef80d403 100644 --- a/src/utils/UtilsGeneral.cpp +++ b/src/utils/UtilsGeneral.cpp @@ -80,7 +80,7 @@ getRulesetName(unsigned int ruleset) QString getAutoRollEnabled(bool autoRollEnabled) { - return autoRollEnabled ? "automatic rolling enabled" : "automatic rolling disabled"; + return autoRollEnabled ? "automatic rolling enabled" : "automatic rolling disabled"; } From d07d0653a9f1bcd20cfa5cf3513937f78e268258 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Thu, 16 Jan 2025 21:38:46 +0100 Subject: [PATCH 11/22] [feature] Icons for move up and down actions --- resources/icons/table/move_down_black.svg | 22 ++++++++++++++++++++++ resources/icons/table/move_down_white.svg | 22 ++++++++++++++++++++++ resources/icons/table/move_up_black.svg | 22 ++++++++++++++++++++++ resources/icons/table/move_up_white.svg | 22 ++++++++++++++++++++++ resources/resources.qrc | 4 ++++ src/ui/table/CombatWidget.cpp | 2 ++ 6 files changed, 94 insertions(+) create mode 100644 resources/icons/table/move_down_black.svg create mode 100644 resources/icons/table/move_down_white.svg create mode 100644 resources/icons/table/move_up_black.svg create mode 100644 resources/icons/table/move_up_white.svg diff --git a/resources/icons/table/move_down_black.svg b/resources/icons/table/move_down_black.svg new file mode 100644 index 00000000..096098ff --- /dev/null +++ b/resources/icons/table/move_down_black.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/table/move_down_white.svg b/resources/icons/table/move_down_white.svg new file mode 100644 index 00000000..648490e6 --- /dev/null +++ b/resources/icons/table/move_down_white.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/table/move_up_black.svg b/resources/icons/table/move_up_black.svg new file mode 100644 index 00000000..e3edab19 --- /dev/null +++ b/resources/icons/table/move_up_black.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/table/move_up_white.svg b/resources/icons/table/move_up_white.svg new file mode 100644 index 00000000..b8742fa1 --- /dev/null +++ b/resources/icons/table/move_up_white.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/resources.qrc b/resources/resources.qrc index 9b00a56b..7153b2fe 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -30,6 +30,10 @@ icons/table/insert_table_white.svg icons/table/log_black.svg icons/table/log_white.svg + icons/table/move_down_black.svg + icons/table/move_down_white.svg + icons/table/move_up_black.svg + icons/table/move_up_white.svg icons/table/redo_black.svg icons/table/redo_white.svg icons/table/remove_black.svg diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index 099277ff..e105acbb 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -335,6 +335,8 @@ CombatWidget::setUndoRedoIcon(bool isDarkMode) m_resortAction->setIcon(isDarkMode ? QIcon(":/icons/table/sort_white.svg") : QIcon(":/icons/table/sort_black.svg")); m_undoAction->setIcon(isDarkMode ? QIcon(":/icons/table/undo_white.svg") : QIcon(":/icons/table/undo_black.svg")); m_redoAction->setIcon(isDarkMode ? QIcon(":/icons/table/redo_white.svg") : QIcon(":/icons/table/redo_black.svg")); + m_moveUpwardAction->setIcon(isDarkMode ? QIcon(":/icons/table/move_up_white.svg") : QIcon(":/icons/table/move_up_black.svg")); + m_moveDownwardAction->setIcon(isDarkMode ? QIcon(":/icons/table/move_down_white.svg") : QIcon(":/icons/table/move_down_black.svg")); m_showLogAction->setIcon(isDarkMode ? QIcon(":/icons/table/log_white.svg") : QIcon(":/icons/table/log_black.svg")); } From eb5db6ec41579ddd9b80c47e27143b21a28a8deb Mon Sep 17 00:00:00 2001 From: Bakefish Date: Thu, 16 Jan 2025 21:39:52 +0100 Subject: [PATCH 12/22] [refactor] minor indentation and general cleanup --- CMakeLists.txt | 8 ++++---- src/ui/MainWindow.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd06311b..d297af2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,11 +11,11 @@ project(LightCombatManager LANGUAGES CXX) find_package(Qt6 QUIET COMPONENTS Widgets) if(${Qt6_FOUND}) - find_package(QT NAMES Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) - find_package(Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) + find_package(QT NAMES Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) + find_package(Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) else() - find_package(QT NAMES Qt5 COMPONENTS Svg Widgets REQUIRED) - find_package(Qt5 COMPONENTS Svg Widgets REQUIRED) + find_package(QT NAMES Qt5 COMPONENTS Svg Widgets REQUIRED) + find_package(Qt5 COMPONENTS Svg Widgets REQUIRED) endif() include(CTest) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 11fb951c..7b820372 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -277,7 +277,7 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) // @note A single immediate call to resize() won't actually resize the window // So the function is called with a minimal delay of 1 ms, which will actually // resize the main window - QTimer::singleShot(1, [this, tableWidth]() { + QTimer::singleShot(1, [this, tableWidth] { resize(tableWidth, height()); }); }); From 90edaad0ebd78826989d5538e74c5b7570bc10c6 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Sat, 18 Jan 2025 14:57:39 +0100 Subject: [PATCH 13/22] [feature] Open Recent menu --- src/ui/MainWindow.cpp | 59 ++++++++++++++++++++++++++++--- src/ui/MainWindow.hpp | 8 ++++- src/ui/settings/DirSettings.cpp | 31 ++++++++++++++++ src/ui/settings/DirSettings.hpp | 4 +++ test/ui/settings/SettingsTest.cpp | 38 ++++++++++++++++---- 5 files changed, 128 insertions(+), 12 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 7b820372..1671692c 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -16,6 +16,8 @@ #include #include +#include + MainWindow::MainWindow() { // Actions @@ -25,7 +27,9 @@ MainWindow::MainWindow() m_openCombatAction = new QAction(tr("&Open..."), this); m_openCombatAction->setShortcuts(QKeySequence::Open); - connect(m_openCombatAction, &QAction::triggered, this, &MainWindow::openTable); + connect(m_openCombatAction, &QAction::triggered, this, [this] { + openTable(); + }); m_saveAction = new QAction(tr("&Save"), this); m_saveAction->setShortcuts(QKeySequence::Save); @@ -55,16 +59,21 @@ MainWindow::MainWindow() auto *const aboutQtAction = new QAction(style()->standardIcon(QStyle::SP_TitleBarMenuButton), tr("About &Qt"), this); connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); + m_openRecentMenu = new QMenu(tr("Open Recent")); + // Add actions auto *const fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(m_newCombatAction); fileMenu->addAction(m_openCombatAction); + fileMenu->addMenu(m_openRecentMenu); fileMenu->addAction(m_saveAction); fileMenu->addAction(m_saveAsAction); fileMenu->addAction(closeAction); fileMenu->addAction(m_openSettingsAction); fileMenu->addSeparator(); + setOpenRecentMenuActions(); + auto *const helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(m_aboutLCMAction); helpMenu->addAction(aboutQtAction); @@ -153,13 +162,23 @@ MainWindow::saveAs() void -MainWindow::openTable() +MainWindow::openTable(const QString& recentDir) { - const auto fileName = QFileDialog::getOpenFileName(this, "Open Table", m_dirSettings.openDir, ("lcm File(*.lcm)")); - // Return if this exact same file is already loaded or if the dialog has been cancelled - if ((m_isTableActive && fileName == m_fileDir) || fileName.isEmpty()) { + QString fileName; + if (recentDir.isEmpty()) { + fileName = QFileDialog::getOpenFileName(this, "Open Table", m_dirSettings.openDir, ("lcm File(*.lcm)")); + if (fileName.isEmpty()) { + return; + } + } else { + fileName = recentDir; + } + + // Return if this exact same file is already loaded + if ((m_isTableActive && fileName == m_fileDir)) { return; } + // Check if a table is active right now if (m_isTableActive && isWindowModified() && createSaveMessageBox(tr("Do you want to save the current Combat before opening another existing Combat?"), false) == 0) { @@ -195,6 +214,7 @@ MainWindow::openTable() m_fileDir = fileName; setTableWidget(true, false); + setOpenRecentMenuActions(); // If the settings rules are applied to the table, it is modified setCombatTitle(rulesModified); break; @@ -405,6 +425,33 @@ MainWindow::checkStoredTableRules(const QJsonObject& jsonObjectData) } +void +MainWindow::setOpenRecentMenuActions() +{ + m_openRecentMenu->clear(); + + if (m_dirSettings.recentDirs.at(0).isEmpty()) { + m_openRecentMenu->addAction(new QAction(tr("No recent dirs"))); + } else { + for (const auto& recentDir : m_dirSettings.recentDirs) { + if (!std::filesystem::exists(recentDir.toStdString())) { + continue; + } + + auto trimmedName = recentDir; + if (trimmedName.length() > 50) { + trimmedName.replace(0, trimmedName.length() - 50, "..."); + } + auto* const recentDirAction = new QAction(trimmedName); + m_openRecentMenu->addAction(recentDirAction); + connect(recentDirAction, &QAction::triggered, this, [this, recentDir] { + openTable(recentDir); + }); + } + } +} + + void MainWindow::setMainWindowIcons() { @@ -417,6 +464,8 @@ MainWindow::setMainWindowIcons() m_openSettingsAction->setIcon(QIcon(isSystemInDarkMode ? ":/icons/menus/gear_white.svg" : ":/icons/menus/gear_black.svg")); m_aboutLCMAction->setIcon(QIcon(isSystemInDarkMode ? ":/icons/logos/main_light.svg" : ":/icons/logos/main_dark.svg")); + m_openRecentMenu->setIcon(QIcon(isSystemInDarkMode ? ":/icons/menus/open_white.svg" : ":/icons/menus/open_black.svg")); + QApplication::setWindowIcon(QIcon(isSystemInDarkMode ? ":/icons/logos/main_light.svg" : ":/icons/logos/main_dark.svg")); } diff --git a/src/ui/MainWindow.hpp b/src/ui/MainWindow.hpp index 568782a9..513f9b65 100644 --- a/src/ui/MainWindow.hpp +++ b/src/ui/MainWindow.hpp @@ -9,6 +9,7 @@ #include class QAction; +class QMenu; class CombatWidget; class WelcomeWidget; @@ -37,7 +38,7 @@ private slots: saveAs(); void - openTable(); + openTable(const QString& recentDir = ""); void openSettings(); @@ -72,6 +73,9 @@ private slots: [[nodiscard]] bool checkStoredTableRules(const QJsonObject& jsonObjectData); + void + setOpenRecentMenuActions(); + void setMainWindowIcons(); @@ -90,6 +94,8 @@ private slots: QPointer m_openSettingsAction; QPointer m_aboutLCMAction; + QPointer m_openRecentMenu; + std::shared_ptr m_tableFileHandler; AdditionalSettings m_additionalSettings; diff --git a/src/ui/settings/DirSettings.cpp b/src/ui/settings/DirSettings.cpp index d7bba8bd..46cf8c43 100644 --- a/src/ui/settings/DirSettings.cpp +++ b/src/ui/settings/DirSettings.cpp @@ -20,6 +20,22 @@ DirSettings::write(const QString& fileName, bool setSaveDir) writeParameter(settings, fileName, saveDir, "dir_save"); saveDir = fileName; } + + // Apply the filename to recent saved directories + if (std::find(std::begin(recentDirs), std::end(recentDirs), fileName) != recentDirs.end()) { + return; + } + // Place in front + std::shift_right(recentDirs.begin(), recentDirs.end(), 1); + recentDirs[0] = fileName; + // Resave recent dir values + for (std::array::size_type i = 0; i < recentDirs.size(); i++) { + if (recentDirs.at(i).isEmpty()) { + break; + } + + settings.setValue("recent_dir_" + QString::number(i), recentDirs.at(i)); + } } @@ -27,6 +43,21 @@ void DirSettings::read() { QSettings settings; + for (std::array::size_type i = 0; i < recentDirs.size(); i++) { + const auto& recentKey = "recent_dir_" + QString::number(i); + if (!settings.value(recentKey).isValid()) { + break; + } + + if (std::filesystem::exists(settings.value(recentKey).toString().toStdString()) + && std::filesystem::path(settings.value(recentKey).toString().toStdString()).extension().string() == ".lcm") { + recentDirs[i] = settings.value("recent_dir_" + QString::number(i)).toString(); + } else { + // Might have turned invalid in the meantime + settings.remove("recent_dir_" + QString::number(i)); + } + } + saveDir = settings.value("dir_save").isValid() ? settings.value("dir_save").toString() : QString(); openDir = settings.value("dir_open").isValid() ? settings.value("dir_open").toString() : QString(); } diff --git a/src/ui/settings/DirSettings.hpp b/src/ui/settings/DirSettings.hpp index 5ff6f531..cb78a6dc 100644 --- a/src/ui/settings/DirSettings.hpp +++ b/src/ui/settings/DirSettings.hpp @@ -4,6 +4,8 @@ #include +#include + // Store data used for handling the opening and saving directories class DirSettings : public BaseSettings { public: @@ -14,6 +16,8 @@ class DirSettings : public BaseSettings { bool setSaveDir = false); public: + std::array recentDirs; + QString openDir; QString saveDir; diff --git a/test/ui/settings/SettingsTest.cpp b/test/ui/settings/SettingsTest.cpp index fa0a3625..2d7dcb41 100644 --- a/test/ui/settings/SettingsTest.cpp +++ b/test/ui/settings/SettingsTest.cpp @@ -11,6 +11,9 @@ #include +#include +#include + TEST_CASE("Settings Testing", "[Settings]") { SECTION("Concepts test") { enum TestEnum {}; @@ -60,18 +63,41 @@ TEST_CASE("Settings Testing", "[Settings]") { QSettings settings; settings.clear(); + // Create file so that the settings entry won't be deleted + std::filesystem::create_directories(std::filesystem::current_path().string() + "/example"); + std::ofstream file(std::filesystem::current_path().string() + "/example/test.lcm"); + file << "Text"; + file.close(); + const auto lcmFilePath = QString::fromStdString(std::filesystem::current_path().string() + "/example/test.lcm"); + REQUIRE(settings.value("dir_save").isValid() == false); REQUIRE(settings.value("dir_open").isValid() == false); + REQUIRE(settings.value("recent_dir_0").isValid() == false); + REQUIRE(settings.value("recent_dir_1").isValid() == false); - dirSettings.write("/example/path/dir_open_and_save", true); + dirSettings.write(lcmFilePath, true); + REQUIRE(settings.value("dir_save").isValid() == true); + REQUIRE(settings.value("dir_open").isValid() == true); + REQUIRE(settings.value("recent_dir_0").isValid() == true); + REQUIRE(settings.value("dir_open").toString() == lcmFilePath); + REQUIRE(settings.value("dir_save").toString() == lcmFilePath); + REQUIRE(settings.value("recent_dir_0").toString() == lcmFilePath); + + dirSettings.write("/example/invalid.csv", false); + REQUIRE(settings.value("dir_open").toString() == "/example/invalid.csv"); + REQUIRE(settings.value("dir_save").toString() == lcmFilePath); + REQUIRE(settings.value("recent_dir_1").isValid() == true); + REQUIRE(settings.value("recent_dir_0").toString() == "/example/invalid.csv"); + REQUIRE(settings.value("recent_dir_1").toString() == lcmFilePath); + + DirSettings newDirSettings; REQUIRE(settings.value("dir_save").isValid() == true); REQUIRE(settings.value("dir_open").isValid() == true); - REQUIRE(settings.value("dir_open").toString() == "/example/path/dir_open_and_save"); - REQUIRE(settings.value("dir_save").toString() == "/example/path/dir_open_and_save"); + // These files never really existed, therefore the settings key should have been deleted + REQUIRE(settings.value("recent_dir_0").isValid() == false); + REQUIRE(settings.value("recent_dir_1").isValid() == true); - dirSettings.write("/example/path/new_path", false); - REQUIRE(settings.value("dir_open").toString() == "/example/path/new_path"); - REQUIRE(settings.value("dir_save").toString() == "/example/path/dir_open_and_save"); + std::filesystem::remove_all("example/test.lcm"); } SECTION("Rule settings test") { RuleSettings ruleSettings; From 5d2458dce9fb275ccd1a5a789b7adf250820b520 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Sat, 18 Jan 2025 15:46:21 +0100 Subject: [PATCH 14/22] [refactor] Cleanup constructor to be more uniform with other classes --- src/ui/MainWindow.cpp | 64 ++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 1671692c..b2739f05 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -21,43 +21,19 @@ MainWindow::MainWindow() { // Actions - m_newCombatAction = new QAction(tr("&New"), this); + m_newCombatAction = new QAction(tr("&New")); m_newCombatAction->setShortcuts(QKeySequence::New); - connect(m_newCombatAction, &QAction::triggered, this, &MainWindow::newCombat); - - m_openCombatAction = new QAction(tr("&Open..."), this); + m_openCombatAction = new QAction(tr("&Open...")); m_openCombatAction->setShortcuts(QKeySequence::Open); - connect(m_openCombatAction, &QAction::triggered, this, [this] { - openTable(); - }); - - m_saveAction = new QAction(tr("&Save"), this); + m_saveAction = new QAction(tr("&Save")); m_saveAction->setShortcuts(QKeySequence::Save); - connect(m_saveAction, &QAction::triggered, this, &MainWindow::saveTable); - - m_saveAsAction = new QAction(tr("&Save As..."), this); + m_saveAsAction = new QAction(tr("&Save As...")); m_saveAsAction->setShortcuts(QKeySequence::SaveAs); - connect(m_saveAsAction, &QAction::triggered, this, &MainWindow::saveAs); - // Both options have to be enabled or disabled simultaneously - connect(this, &MainWindow::setSaveAction, this, [this] (bool enable) { - m_saveAction->setEnabled(enable); - m_saveAsAction->setEnabled(enable); - }); - - auto* const closeAction = new QAction(QIcon(":/icons/menus/close.svg"), tr("&Close"), this); - closeAction->setShortcuts(QKeySequence::Close); - connect(closeAction, &QAction::triggered, this, [this] () { - m_isTableActive ? exitCombat() : QApplication::quit(); - }); - - m_openSettingsAction = new QAction(tr("Settings..."), this); - connect(m_openSettingsAction, &QAction::triggered, this, &MainWindow::openSettings); + m_openSettingsAction = new QAction(tr("Settings...")); + m_aboutLCMAction = new QAction(tr("&About LCM")); - m_aboutLCMAction = new QAction(tr("&About LCM"), this); - connect(m_aboutLCMAction, &QAction::triggered, this, &MainWindow::about); - - auto *const aboutQtAction = new QAction(style()->standardIcon(QStyle::SP_TitleBarMenuButton), tr("About &Qt"), this); - connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); + auto* const closeAction = new QAction(QIcon(":/icons/menus/close.svg"), tr("&Close")); + auto *const aboutQtAction = new QAction(style()->standardIcon(QStyle::SP_TitleBarMenuButton), tr("About &Qt")); m_openRecentMenu = new QMenu(tr("Open Recent")); @@ -72,17 +48,35 @@ MainWindow::MainWindow() fileMenu->addAction(m_openSettingsAction); fileMenu->addSeparator(); - setOpenRecentMenuActions(); - auto *const helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(m_aboutLCMAction); helpMenu->addAction(aboutQtAction); + connect(m_newCombatAction, &QAction::triggered, this, &MainWindow::newCombat); + connect(m_openCombatAction, &QAction::triggered, this, [this] { + openTable(); + }); + connect(m_saveAction, &QAction::triggered, this, &MainWindow::saveTable); + connect(m_saveAsAction, &QAction::triggered, this, &MainWindow::saveAs); + closeAction->setShortcuts(QKeySequence::Close); + connect(closeAction, &QAction::triggered, this, [this] () { + m_isTableActive ? exitCombat() : QApplication::quit(); + }); + connect(m_openSettingsAction, &QAction::triggered, this, &MainWindow::openSettings); + connect(m_aboutLCMAction, &QAction::triggered, this, &MainWindow::about); + connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt); + // Both options have to be enabled or disabled simultaneously + connect(this, &MainWindow::setSaveAction, this, [this] (bool enable) { + m_saveAction->setEnabled(enable); + m_saveAsAction->setEnabled(enable); + }); + m_tableFileHandler = std::make_shared(); + setOpenRecentMenuActions(); setMainWindowIcons(); - resize(START_WIDTH, START_HEIGHT); setWelcomingWidget(); + resize(START_WIDTH, START_HEIGHT); } From 6ff5740311f01fe2be348907710a37ebc366929d Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 14 Feb 2025 22:16:29 +0100 Subject: [PATCH 15/22] [infrastructure] Remove Qt5 support --- .github/workflows/run-mac.yml | 18 ++---------- .github/workflows/run-ubuntu.yml | 21 ++++---------- .github/workflows/run-windows.yml | 48 +++---------------------------- CMakeLists.txt | 11 ++----- README.md | 11 ++++--- 5 files changed, 19 insertions(+), 90 deletions(-) diff --git a/.github/workflows/run-mac.yml b/.github/workflows/run-mac.yml index 1833193a..7a0ee134 100644 --- a/.github/workflows/run-mac.yml +++ b/.github/workflows/run-mac.yml @@ -18,26 +18,14 @@ jobs: - name: Install Catch2 run: brew install catch2 - - name: Install Qt5 - run: brew install qt@5 - - - name: Configure CMake - Qt5 - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" - - - name: Build Project - Qt5 - run: cd build && cmake .. - - - name: Test - Qt5 - run: cd build && make tests && cd test && ./tests - - name: Install Qt6 run: brew install qt@6 - - name: Configure CMake - Qt6 + - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)" - - name: Build Project - Qt6 + - name: Build Project run: cd build && cmake .. - - name: Test - Qt6 + - name: Test run: cd build && make tests && cd test && ./tests diff --git a/.github/workflows/run-ubuntu.yml b/.github/workflows/run-ubuntu.yml index 1fb9967a..4c1beddd 100644 --- a/.github/workflows/run-ubuntu.yml +++ b/.github/workflows/run-ubuntu.yml @@ -12,22 +12,11 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Install dependencies - Qt5 - run: sudo apt update && sudo apt install -y make cmake gcc qtbase5-dev libqt5svg5 libqt5svg5-dev catch2 - - name: Configure CMake - Qt5 + - name: Install dependencies + run: sudo apt update && sudo apt install -y make cmake gcc qt6-base-dev libqt6svg6 libqt6svg6-dev catch2 + - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - - name: Build project - Qt5 + - name: Build project run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - name: Test - Qt5 - run: cmake --build build --target test - - - name: Install Qt6 - run: sudo apt update && sudo apt install -y qt6-base-dev libqt6svg6 libqt6svg6-dev - - name: Clear directory - run: rm -rf ${{github.workspace}}/build - - name: Configure CMake - Qt6 - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - - name: Build project - Qt6 - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - name: Test - Qt6 + - name: Test run: cmake --build build --target test diff --git a/.github/workflows/run-windows.yml b/.github/workflows/run-windows.yml index 0b943fad..d2a94da2 100644 --- a/.github/workflows/run-windows.yml +++ b/.github/workflows/run-windows.yml @@ -30,46 +30,6 @@ jobs: cmake .. -DBUILD_TESTING=OFF; cmake --build . --config Release --target install shell: powershell - - name: Install Qt5 - uses: jurplel/install-qt-action@v4 - with: - version: '${{env.QT5_VERSION}}' - host: 'windows' - target: 'desktop' - arch: 'win64_msvc2019_64' - archives: 'qtbase qtsvg' - cache: 'false' - cache-key-prefix: 'install-qt-action' - install-deps: 'true' - setup-python: 'false' - set-env: 'true' - - - name: Edit CMakeLists.txt - Qt5 - run: | - Add-Content ${{github.workspace}}/CMakeLists.txt 'set(CMAKE_PREFIX_PATH "${{runner.workspace}}/Qt/${{env.QT5_VERSION}}/msvc2019_64/")' - (Get-Content ${{github.workspace}}/CMakeLists.txt).Replace('\', '/') | Set-Content ${{github.workspace}}/CMakeLists.txt - shell: powershell - - - name: Configure CMake - Qt5 - run: | - cmake -B build -G "Visual Studio 17 2022" -DCMAKE_CXX_COMPILER=cl - - shell: powershell - - name: Build Project - Qt5 - run: | - cmake --build build --config ${{env.BUILD_TYPE}} - shell: powershell - - - name: Test - Qt5 - run: | - ${{github.workspace}}/build/test/${{env.BUILD_TYPE}}/tests.exe - shell: powershell - - - name: Clear directory - run: | - Remove-Item "${{github.workspace}}/build" -Recurse -Include *.* - shell: powershell - - name: Install Qt6 uses: jurplel/install-qt-action@v4 with: @@ -84,21 +44,21 @@ jobs: setup-python: 'false' set-env: 'true' - - name: Edit CMakeLists.txt - Qt6 + - name: Edit CMakeLists.txt run: | (Get-Content ${{github.workspace}}/CMakeLists.txt).Replace('${{env.QT5_VERSION}}', '${{env.QT6_VERSION}}') | Set-Content ${{github.workspace}}/CMakeLists.txt shell: powershell - - name: Configure CMake - Qt6 + - name: Configure CMake run: | cmake -B build -G "Visual Studio 17 2022" -DCMAKE_CXX_COMPILER=cl - - name: Build Project - Qt6 + - name: Build Project run: | cmake --build build --config ${{env.BUILD_TYPE}} shell: powershell - - name: Test - Qt6 + - name: Test run: | ${{github.workspace}}/build/test/${{env.BUILD_TYPE}}/tests.exe shell: powershell diff --git a/CMakeLists.txt b/CMakeLists.txt index d297af2a..87b24932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,15 +8,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) project(LightCombatManager LANGUAGES CXX) -find_package(Qt6 QUIET COMPONENTS Widgets) - -if(${Qt6_FOUND}) - find_package(QT NAMES Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) - find_package(Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) -else() - find_package(QT NAMES Qt5 COMPONENTS Svg Widgets REQUIRED) - find_package(Qt5 COMPONENTS Svg Widgets REQUIRED) -endif() +find_package(QT NAMES Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) +find_package(Qt6 COMPONENTS SvgWidgets Widgets REQUIRED) include(CTest) enable_testing() diff --git a/README.md b/README.md index e5f4d74f..0d8909df 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,7 @@ Support for more d20-based rulesets might be added in the future. # Tools & Installation LCM is written in C++20. The following frameworks are used for development: -* [Qt6 or Qt5](https://www.qt.io/) for the user interface and the storing and loading of tables. - * If no Qt6 installation is found on the system, the application searches for a Qt5 installation instead. +* [Qt6](https://www.qt.io/) for the user interface and the storing and loading of tables. * Note that for the correct displaying of svg files, the Qt SVG plugin is needed. * [Catch2 v2 or v3](https://github.com/catchorg/Catch2) for Unit testing ([Catch2 license](https://github.com/catchorg/Catch2/blob/devel/LICENSE.txt)). * [Uncrustify](https://github.com/uncrustify/uncrustify) for code formatting. @@ -49,11 +48,11 @@ LCM is written in C++20. The following frameworks are used for development: The following commands will install all necessary requirements at once: ### Ubuntu: -`sudo apt install qtbase5-dev libqt5svg5 libqt5svg5-dev qt6-base-dev libqt6svg6 libqt6svg6-dev catch2 uncrustify cmake` +`sudo apt install qt6-base-dev libqt6svg6 libqt6svg6-dev catch2 uncrustify cmake` ### Arch Linux: -`sudo pacman -S qt5-base qt5-svg qt6-base qt6-svg catch2 uncrustify cmake` +`sudo pacman -S qt6-base qt6-svg catch2 uncrustify cmake` ### MacOS: -`brew install qt@5 qt@6 catch2 uncrustify cmake` +`brew install qt@6 catch2 uncrustify cmake` For Windows, installers for Qt, CMake and Catch2 are available. Make sure to install the Qt SVG plugin as well!\ Alternatively, if you want to run the application without any additional installing, just download the binaries provided with the latest release. @@ -62,7 +61,7 @@ Alternatively, if you want to run the application without any additional install 1. Clone this repository and `cd` into it. 2. `mkdir build && cd build` -3. On MacOS: Hit `cmake -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)"` (Change to `qt@6` for Qt6), on Linux: Hit `cmake ..` +3. On MacOS: Hit `cmake -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"`, on Linux: Hit `cmake ..` 4. `make` 5. Start the application with `./src/LightCombatManager`. From f548afebf5eca44e8b54140fde78cf78d6e06c37 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 14 Feb 2025 22:29:38 +0100 Subject: [PATCH 16/22] [refactor] Apply emplace semantics on vectors --- src/handler/char/CharacterHandler.cpp | 2 +- src/ui/table/CombatTableWidget.cpp | 4 ++-- src/ui/table/CombatWidget.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handler/char/CharacterHandler.cpp b/src/handler/char/CharacterHandler.cpp index 3e6ad5e9..a1142812 100644 --- a/src/handler/char/CharacterHandler.cpp +++ b/src/handler/char/CharacterHandler.cpp @@ -12,7 +12,7 @@ CharacterHandler::storeCharacter( bool isEnemy, AdditionalInfoData additionalInfoData) { - characters.push_back(Character(name, initiative, modifier, hp, isEnemy, additionalInfoData)); + characters.emplace_back(Character(name, initiative, modifier, hp, isEnemy, additionalInfoData)); } diff --git a/src/ui/table/CombatTableWidget.cpp b/src/ui/table/CombatTableWidget.cpp index 6aefa7aa..b68f0212 100644 --- a/src/ui/table/CombatTableWidget.cpp +++ b/src/ui/table/CombatTableWidget.cpp @@ -174,10 +174,10 @@ CombatTableWidget::tableDataFromWidget() QVector rowValues; for (auto j = 0; j < FIRST_FOUR_COLUMNS; j++) { - rowValues.push_back(item(i, j)->text()); + rowValues.emplace_back(item(i, j)->text()); } - rowValues.push_back(item(i, Utils::Table::COL_ENEMY)->checkState() == Qt::Checked); + rowValues.emplace_back(item(i, Utils::Table::COL_ENEMY)->checkState() == Qt::Checked); QVariant variant; variant.setValue(cellWidget(i, Utils::Table::COL_ADDITIONAL)->findChild()->getAdditionalInformation()); diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index e105acbb..edcf841a 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -841,7 +841,7 @@ CombatWidget::loadCharactersFromTable(const QJsonObject& jsonObject) additionalInfoData.statusEffects.push_back(effect); } - characters.push_back(CharacterHandler::Character { + characters.emplace_back(CharacterHandler::Character { characterObject.value("name").toString(), characterObject.value("initiative").toInt(), characterObject.value("modifier").toInt(), characterObject.value("hp").toInt(), characterObject.value("is_enemy").toBool(), additionalInfoData }); From 30ad1550c07e20c450332af2357e6b86deebce34 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 18 Apr 2025 15:53:45 +0200 Subject: [PATCH 17/22] [fix] Adjusted table height regardless of option setting --- src/ui/table/CombatWidget.cpp | 3 ++- src/ui/table/Undo.cpp | 12 +++++++++--- src/ui/table/Undo.hpp | 4 +++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ui/table/CombatWidget.cpp b/src/ui/table/CombatWidget.cpp index edcf841a..8884dc7f 100644 --- a/src/ui/table/CombatWidget.cpp +++ b/src/ui/table/CombatWidget.cpp @@ -285,7 +285,8 @@ CombatWidget::pushOnUndoStack(bool resynchronize) // We got everything, so push m_undoStack->push(new Undo(this, m_logListWidget, m_roundCounterLabel, m_currentPlayerLabel, oldData, newData, m_affectedRowIndices, &m_rowEntered, &m_roundCounter, - m_tableSettings.colorTableRows, m_tableSettings.showIniToolTips)); + m_tableSettings.colorTableRows, m_tableSettings.showIniToolTips, + m_tableSettings.adjustHeightAfterRemove)); m_affectedRowIndices.clear(); } diff --git a/src/ui/table/Undo.cpp b/src/ui/table/Undo.cpp index 4c2fc276..2e73e39f 100644 --- a/src/ui/table/Undo.cpp +++ b/src/ui/table/Undo.cpp @@ -12,12 +12,12 @@ Undo::Undo(CombatWidget *CombatWidget, LogListWidget* logListWidget, QPointer roundCounterLabel, QPointer currentPlayerLabel, const UndoData& oldData, const UndoData& newData, const std::vector affectedRows, unsigned int* rowEntered, unsigned int* roundCounter, - bool colorTableRows, bool showIniToolTips) : + bool colorTableRows, bool showIniToolTips, bool adjustTableHeight) : m_combatWidget(CombatWidget), m_logListWidget(logListWidget), m_roundCounterLabel(roundCounterLabel), m_currentPlayerLabel(currentPlayerLabel), m_oldData(std::move(oldData)), m_newData(std::move(newData)), m_affectedRows(std::move(affectedRows)), m_rowEntered(rowEntered), m_roundCounter(roundCounter), - m_colorTableRows(colorTableRows), m_showIniToolTips(showIniToolTips) + m_colorTableRows(colorTableRows), m_showIniToolTips(showIniToolTips), m_adjustTableHeight(adjustTableHeight) { } @@ -83,7 +83,13 @@ Undo::setCombatWidget(bool undo) tableWidget->setTableRowColor(!m_colorTableRows); tableWidget->setIniColumnTooltips(!m_showIniToolTips); - emit m_combatWidget->tableHeightSet(tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); + // Only set table height if undoing and not removing + // (this is handled in the combat widget's remove row function) + const auto isRemovingRow = (oldTableData.size() < newTableData.size() && undo) || + (oldTableData.size() > newTableData.size() && !undo); + if (undo && !isRemovingRow && m_adjustTableHeight) { + emit m_combatWidget->tableHeightSet(tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); + } emit m_combatWidget->changeOccured(); tableWidget->blockSignals(false); diff --git a/src/ui/table/Undo.hpp b/src/ui/table/Undo.hpp index 0c40c70b..675d0794 100644 --- a/src/ui/table/Undo.hpp +++ b/src/ui/table/Undo.hpp @@ -29,7 +29,8 @@ class Undo : public QUndoCommand { unsigned int* rowEntered, unsigned int* roundCounter, bool colorTableRows, - bool showIniToolTips); + bool showIniToolTips, + bool adjustTableHeight); void undo() override; @@ -66,6 +67,7 @@ class Undo : public QUndoCommand { const bool m_colorTableRows; const bool m_showIniToolTips; + const bool m_adjustTableHeight; static constexpr int COL_ENEMY = 4; static constexpr int COL_ADDITIONAL = 5; From e2c79f0f79d1016936a71ee2a8477ccd35ae0a98 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 18 Apr 2025 16:34:52 +0200 Subject: [PATCH 18/22] [fix] Did not process row removal and return to main window resizes correctly --- src/ui/MainWindow.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index b2739f05..741798d2 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -268,7 +268,9 @@ MainWindow::setWelcomingWidget() m_welcomeWidget = new WelcomeWidget(this); setCentralWidget(m_welcomeWidget); - resize(START_WIDTH, START_HEIGHT); + QTimer::singleShot(1, [this] { + resize(START_WIDTH, START_HEIGHT); + }); m_isTableSavedInFile = false; emit setSaveAction(false); @@ -282,10 +284,9 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) setCentralWidget(m_combatWidget); connect(m_combatWidget, &CombatWidget::exit, this, &MainWindow::exitCombat); connect(m_combatWidget, &CombatWidget::tableHeightSet, this, [this] (unsigned int height) { - if (height <= START_HEIGHT) { - return; - } - resize(width(), height); + QTimer::singleShot(1, [this, height] { + resize(width(), height); + }); }); connect(m_combatWidget, &CombatWidget::tableWidthSet, this, [this] (int tableWidth) { // @note A single immediate call to resize() won't actually resize the window From a34b98559f61e1ce30351548b11dd8e8284be352 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 18 Apr 2025 16:39:26 +0200 Subject: [PATCH 19/22] [refactor] Generalize to one timed resize function --- src/ui/MainWindow.cpp | 35 +++++++++++++++-------------------- src/ui/MainWindow.hpp | 4 ++++ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 741798d2..6ae31960 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -268,9 +268,7 @@ MainWindow::setWelcomingWidget() m_welcomeWidget = new WelcomeWidget(this); setCentralWidget(m_welcomeWidget); - QTimer::singleShot(1, [this] { - resize(START_WIDTH, START_HEIGHT); - }); + callTimedResize(START_WIDTH, START_HEIGHT); m_isTableSavedInFile = false; emit setSaveAction(false); @@ -284,17 +282,10 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) setCentralWidget(m_combatWidget); connect(m_combatWidget, &CombatWidget::exit, this, &MainWindow::exitCombat); connect(m_combatWidget, &CombatWidget::tableHeightSet, this, [this] (unsigned int height) { - QTimer::singleShot(1, [this, height] { - resize(width(), height); - }); + callTimedResize(width(), height); }); connect(m_combatWidget, &CombatWidget::tableWidthSet, this, [this] (int tableWidth) { - // @note A single immediate call to resize() won't actually resize the window - // So the function is called with a minimal delay of 1 ms, which will actually - // resize the main window - QTimer::singleShot(1, [this, tableWidth] { - resize(tableWidth, height()); - }); + callTimedResize(tableWidth, height()); }); connect(m_combatWidget, &CombatWidget::changeOccured, this, [this] { setCombatTitle(true); @@ -302,20 +293,14 @@ MainWindow::setTableWidget(bool isDataStored, bool newCombatStarted) setCombatTitle(false); - const auto resizeWidget = [this] (int width, int height) { - QTimer::singleShot(1, [this, width, height] { - resize(width, height); - }); - }; - if (newCombatStarted) { - resizeWidget(START_WIDTH, START_HEIGHT); + callTimedResize(START_WIDTH, START_HEIGHT); m_combatWidget->openAddCharacterDialog(); } else { m_combatWidget->generateTableFromTableData(); const auto width = m_combatWidget->isLoggingWidgetVisible() ? m_combatWidget->width() - 250 : m_combatWidget->width(); const auto height = m_combatWidget->getHeight(); - resizeWidget(std::max(width, START_WIDTH), std::max(height, START_HEIGHT)); + callTimedResize(std::max(width, START_WIDTH), std::max(height, START_HEIGHT)); } m_isTableActive = true; @@ -465,6 +450,16 @@ MainWindow::setMainWindowIcons() } +void +MainWindow::callTimedResize(int width, int height) +{ + // Sometimes it needs minimal delays to process events in the background before this can be called + QTimer::singleShot(1, [this, width, height] { + resize(width, height); + }); +} + + bool MainWindow::event(QEvent *event) { diff --git a/src/ui/MainWindow.hpp b/src/ui/MainWindow.hpp index 513f9b65..8c1fcf34 100644 --- a/src/ui/MainWindow.hpp +++ b/src/ui/MainWindow.hpp @@ -79,6 +79,10 @@ private slots: void setMainWindowIcons(); + void + callTimedResize(int width, + int height); + bool event(QEvent *event) override; From 315eda1debb3e3abb0df6770a3064c1cee621a34 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 18 Apr 2025 16:46:08 +0200 Subject: [PATCH 20/22] [version] Adjust UI to new version --- src/ui/MainWindow.cpp | 2 +- src/ui/WelcomeWidget.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 6ae31960..faf7dc8a 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -243,7 +243,7 @@ MainWindow::about() QMessageBox::about(this, tr("About Light Combat Manager"), tr("

Light Combat Manager. A small, lightweight combat manager for d20-based role playing games.
" "Code available on Github. Uses GNU GPLv3 license.

" - "

Version 3.0.0.
" + "

Version 3.1.0.
" "Changelog

")); } diff --git a/src/ui/WelcomeWidget.cpp b/src/ui/WelcomeWidget.cpp index a38523db..6463d269 100644 --- a/src/ui/WelcomeWidget.cpp +++ b/src/ui/WelcomeWidget.cpp @@ -17,8 +17,8 @@ WelcomeWidget::WelcomeWidget(QWidget *parent) "or open an already existing Combat ('File' -> 'Open...').")); welcomeLabel->setAlignment(Qt::AlignCenter); - auto* const versionLabel = new QLabel("v3.0.0"); - versionLabel->setToolTip(tr("Logging widget, major infrastructure updates and various UI improvements!")); + auto* const versionLabel = new QLabel("v3.1.0"); + versionLabel->setToolTip(tr("Custom effects, an Open Recent Menu and minor icon rearrangement!")); versionLabel->setAlignment(Qt::AlignRight); auto *const layout = new QVBoxLayout(this); From 3c8a70bcd27aadae8db76ca58d5b6c52a4de0355 Mon Sep 17 00:00:00 2001 From: Bakefish Date: Fri, 18 Apr 2025 16:52:17 +0200 Subject: [PATCH 21/22] [fix] Templates were sometimes unsorted --- src/ui/table/dialog/template/TemplatesWidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/table/dialog/template/TemplatesWidget.cpp b/src/ui/table/dialog/template/TemplatesWidget.cpp index a90a78cb..6869ed80 100644 --- a/src/ui/table/dialog/template/TemplatesWidget.cpp +++ b/src/ui/table/dialog/template/TemplatesWidget.cpp @@ -53,6 +53,7 @@ TemplatesWidget::loadTemplates() } } + m_templatesListWidget->sortItems(); if (m_templatesListWidget->count() > 0) { m_templatesListWidget->item(0)->setSelected(true); } From 5513a77d9881b2a40b1b48151e7b0cfc9f113f35 Mon Sep 17 00:00:00 2001 From: Maxime Fleury Date: Fri, 18 Apr 2025 17:05:13 +0200 Subject: [PATCH 22/22] [readme] Adjust to 3.1.0 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0d8909df..c6fcef37 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![CI MacOS badge](https://github.com/MaxFleur/LightCombatManager/actions/workflows/run-mac.yml/badge.svg?event=push) ![CI Ubuntu badge](https://github.com/MaxFleur/LightCombatManager/actions/workflows/run-ubuntu.yml/badge.svg?event=push) ![CI Windows badge](https://github.com/MaxFleur/LightCombatManager/actions/workflows/run-windows.yml/badge.svg?event=push) - ![Tag badge](https://img.shields.io/badge/Release-v3.0.0-blue.svg) + ![Tag badge](https://img.shields.io/badge/Release-v3.1.0-blue.svg) @@ -17,11 +17,11 @@ ### A small, lightweight cross-platform combat manager for d20-based role-playing games, based on Qt. -![bossfight](https://github.com/user-attachments/assets/a07db4f2-0c9f-451a-9143-4c4e774833e3) +![overall](https://github.com/user-attachments/assets/b8141a35-754b-460d-8cca-db51add3207a) With LightCombatManager (or just **LCM**), you can easily manage a combat for a d20-based RPG. The table supports all sorts of combat-based operations, such as reordering rows when a character moves their initiative, removing or adding ruleset-defined status effects to one or multiple characters or subsequent addition of characters who just joined the combat. Undoing and logging changes are also supported! -![editor](https://github.com/user-attachments/assets/d925de4c-28d6-427c-ac4b-1250ba421cad) +![char_editor](https://github.com/user-attachments/assets/353a4b5a-6e27-4fad-94b7-17d8256a4929) LCM provides an intuitive character editor, where characters with initiative value and modifier, a health point counter and additional information can be easily created.\ If the game ends, but the current combat is not finished yet, you can save it on the PC. Characters can also be stored as templates for later usage. @@ -40,7 +40,7 @@ Support for more d20-based rulesets might be added in the future. # Tools & Installation LCM is written in C++20. The following frameworks are used for development: -* [Qt6](https://www.qt.io/) for the user interface and the storing and loading of tables. +* [Qt6](https://www.qt.io/) for the user interface as well as table storage and loading. * Note that for the correct displaying of svg files, the Qt SVG plugin is needed. * [Catch2 v2 or v3](https://github.com/catchorg/Catch2) for Unit testing ([Catch2 license](https://github.com/catchorg/Catch2/blob/devel/LICENSE.txt)). * [Uncrustify](https://github.com/uncrustify/uncrustify) for code formatting.