diff --git a/.github/workflows/run-mac.yml b/.github/workflows/run-mac.yml index 1833193..7a0ee13 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 1fb9967..4c1bedd 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 0b943fa..d2a94da 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 fd06311..87b2493 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 e5f4d74..c6fcef3 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,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 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. @@ -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`. diff --git a/resources/icons/table/move_down_black.svg b/resources/icons/table/move_down_black.svg new file mode 100644 index 0000000..096098f --- /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 0000000..648490e --- /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 0000000..e3edab1 --- /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 0000000..b8742fa --- /dev/null +++ b/resources/icons/table/move_up_white.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/table/remove_black.svg b/resources/icons/table/remove_black.svg index e9ab77a..3ba8333 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 82e7f1f..d322bd3 100644 --- a/resources/icons/table/remove_white.svg +++ b/resources/icons/table/remove_white.svg @@ -1,20 +1,11 @@ - - - - - - - - - - + diff --git a/resources/resources.qrc b/resources/resources.qrc index 9b00a56..7153b2f 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/handler/char/CharacterHandler.cpp b/src/handler/char/CharacterHandler.cpp index 3e6ad5e..a114281 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/handler/file/BaseFileHandler.cpp b/src/handler/file/BaseFileHandler.cpp index ee6a65e..96cab44 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 010fa44..bd31020 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/CMakeLists.txt b/src/handler/file/CMakeLists.txt index 14409ad..970a1cb 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/CharFileHandler.cpp b/src/handler/file/CharFileHandler.cpp index 7180123..686bb98 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 ed25418..16927b5 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 new file mode 100644 index 0000000..f021e44 --- /dev/null +++ b/src/handler/file/EffectFileHandler.cpp @@ -0,0 +1,37 @@ +#include "EffectFileHandler.hpp" + +#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; + + return BaseFileHandler::writeJsonObjectToFile(effectObject, m_directoryString + "/" + name + ".effect"); +} + + +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 0000000..b639c2e --- /dev/null +++ b/src/handler/file/EffectFileHandler.hpp @@ -0,0 +1,32 @@ +#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; + + // 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/handler/file/TableFileHandler.cpp b/src/handler/file/TableFileHandler.cpp index 403cdde..a2193f8 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/MainWindow.cpp b/src/ui/MainWindow.cpp index 739e72d..faf7dc8 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -16,49 +16,32 @@ #include #include +#include + 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, &MainWindow::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); - }); + m_openSettingsAction = new QAction(tr("Settings...")); + m_aboutLCMAction = new QAction(tr("&About LCM")); - 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(); - }); + 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_openSettingsAction = new QAction(tr("Settings..."), this); - connect(m_openSettingsAction, &QAction::triggered, this, &MainWindow::openSettings); - - 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); + 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); @@ -69,11 +52,31 @@ MainWindow::MainWindow() 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); } @@ -153,13 +156,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) { @@ -174,8 +187,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) { @@ -195,6 +208,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; @@ -229,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

")); } @@ -254,7 +268,7 @@ MainWindow::setWelcomingWidget() m_welcomeWidget = new WelcomeWidget(this); setCentralWidget(m_welcomeWidget); - resize(START_WIDTH, START_HEIGHT); + callTimedResize(START_WIDTH, START_HEIGHT); m_isTableSavedInFile = false; emit setSaveAction(false); @@ -268,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) { - if (height > START_HEIGHT && (int) height > 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); @@ -286,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; @@ -359,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; } @@ -404,6 +405,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() { @@ -416,10 +444,22 @@ 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")); } +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 568782a..8c1fcf3 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,9 +73,16 @@ private slots: [[nodiscard]] bool checkStoredTableRules(const QJsonObject& jsonObjectData); + void + setOpenRecentMenuActions(); + void setMainWindowIcons(); + void + callTimedResize(int width, + int height); + bool event(QEvent *event) override; @@ -90,6 +98,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/WelcomeWidget.cpp b/src/ui/WelcomeWidget.cpp index a38523d..6463d26 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); diff --git a/src/ui/settings/DirSettings.cpp b/src/ui/settings/DirSettings.cpp index d7bba8b..46cf8c4 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 5ff6f53..cb78a6d 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/src/ui/settings/TableSettings.cpp b/src/ui/settings/TableSettings.cpp index fb638a3..e23305b 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 7043a8d..20fc2ea 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/CombatTableWidget.cpp b/src/ui/table/CombatTableWidget.cpp index 4a478ee..b68f021 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()); @@ -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 9dde0dd..c107b79 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 20d0ac4..8884dc7 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); @@ -284,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(); } @@ -323,9 +325,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")); @@ -334,6 +336,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")); } @@ -343,15 +347,14 @@ 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) { 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(); }); if (dialog->exec() == QDialog::Accepted) { @@ -415,7 +418,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); @@ -615,6 +618,10 @@ CombatWidget::removeRow() setRowAndPlayer(); pushOnUndoStack(); m_tableWidget->itemSelectionChanged(); + + if (m_tableSettings.adjustHeightAfterRemove) { + emit tableHeightSet(m_tableWidget->getHeight() + Utils::Table::HEIGHT_BUFFER); + } } @@ -800,6 +807,7 @@ CombatWidget::setTableOption(bool option, int valueType) case 3: m_tableWidget->setIniColumnTooltips(!option); break; + case 4: default: break; } @@ -834,7 +842,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 }); @@ -860,15 +868,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) { @@ -922,5 +932,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/src/ui/table/CombatWidget.hpp b/src/ui/table/CombatWidget.hpp index 1e9bb05..7294d18 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; diff --git a/src/ui/table/Undo.cpp b/src/ui/table/Undo.cpp index 1f547a7..2e73e39 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()); + // 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 0c40c70..675d079 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; diff --git a/src/ui/table/dialog/AddCustomEffectDialog.cpp b/src/ui/table/dialog/AddCustomEffectDialog.cpp new file mode 100644 index 0000000..9c9d6e2 --- /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 0000000..e07c78c --- /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 2f76bdd..0e2cacc 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 2850a41..ef82420 100644 --- a/src/ui/table/dialog/StatusEffectDialog.cpp +++ b/src/ui/table/dialog/StatusEffectDialog.cpp @@ -1,18 +1,23 @@ #include "StatusEffectDialog.hpp" +#include "AddCustomEffectDialog.hpp" #include "RuleSettings.hpp" #include "StatusEffectData.hpp" +#include "UtilsFiles.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 +25,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 +35,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 +109,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 +118,105 @@ 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; + } + + 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; + } + 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 +239,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 51de83d..f001af5 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/src/ui/table/dialog/template/TemplatesListWidget.cpp b/src/ui/table/dialog/template/TemplatesListWidget.cpp index e3dc869..a1c4e0d 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 aa97066..740271b 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 897fd44..6869ed8 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) : @@ -52,6 +53,7 @@ TemplatesWidget::loadTemplates() } } + m_templatesListWidget->sortItems(); if (m_templatesListWidget->count() > 0) { m_templatesListWidget->item(0)->setSelected(true); } @@ -93,12 +95,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 (!m_charFileHandler->removeCharacter(character.name + ".char")) { - 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/CMakeLists.txt b/src/utils/CMakeLists.txt index bca92a3..c4dbcd8 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 0000000..d775d75 --- /dev/null +++ b/src/utils/UtilsFiles.cpp @@ -0,0 +1,41 @@ +#include "UtilsFiles.hpp" + +#include +#include +#include +#include + +namespace Utils::Files +{ +bool +removeFile(const QString& fileName) +{ + QFile file(fileName); + if (!file.exists()) { + return false; + } + + 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 new file mode 100644 index 0000000..7a6a152 --- /dev/null +++ b/src/utils/UtilsFiles.hpp @@ -0,0 +1,19 @@ +#pragma once + +#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/src/utils/UtilsGeneral.cpp b/src/utils/UtilsGeneral.cpp index a408512..ef80d40 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"; } diff --git a/src/utils/UtilsTable.hpp b/src/utils/UtilsTable.hpp index f4b5411..034fb46 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; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8cc7bb..d864acf 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 @@ -20,7 +21,8 @@ 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 ) target_link_libraries(tests diff --git a/test/handler/CharFileHandlerTest.cpp b/test/handler/CharFileHandlerTest.cpp index 3b5833c..6ca0a1d 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 new file mode 100644 index 0000000..3e0d7b6 --- /dev/null +++ b/test/handler/EffectFileHandlerTest.cpp @@ -0,0 +1,46 @@ +#include "EffectFileHandler.hpp" + +#ifdef CATCH2_V3 +#include +#else +#include +#endif + +#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["broken"] = 2; + // Write to file + effectFileHandler->writeJsonObjectToFile(jsonObject, dir.currentPath() + "/effects/Broken Effect.effect"); + + 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); + } + + std::remove("./effects/Test Effect.effect"); +} diff --git a/test/handler/TableFileHandlerTest.cpp b/test/handler/TableFileHandlerTest.cpp index 5414481..d132d1e 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/ui/settings/SettingsTest.cpp b/test/ui/settings/SettingsTest.cpp index a307011..2d7dcb4 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("dir_open").toString() == "/example/path/dir_open_and_save"); - REQUIRE(settings.value("dir_save").toString() == "/example/path/dir_open_and_save"); + 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); + // 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; @@ -102,23 +128,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(); } } diff --git a/test/ui/widget/TemplatesListWidgetTest.cpp b/test/ui/widget/TemplatesListWidgetTest.cpp index 354fc34..b6bd07a 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 new file mode 100644 index 0000000..958bbce --- /dev/null +++ b/test/utils/UtilsFilesTest.cpp @@ -0,0 +1,37 @@ +#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 == 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()); + } +} diff --git a/test/utils/GeneralUtilsTest.cpp b/test/utils/UtilsGeneralTest.cpp similarity index 84% rename from test/utils/GeneralUtilsTest.cpp rename to test/utils/UtilsGeneralTest.cpp index be852de..0d9113e 100644 --- a/test/utils/GeneralUtilsTest.cpp +++ b/test/utils/UtilsGeneralTest.cpp @@ -1,4 +1,3 @@ -#include "AdditionalInfoWidget.hpp" #include "UtilsGeneral.hpp" #ifdef CATCH2_V3 @@ -7,9 +6,7 @@ #include #endif -#include - -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");