diff --git a/CI/before_install/linux_qt5.sh b/CI/before_install/linux_qt5.sh index ebf9faeb1..6e98dc5dd 100644 --- a/CI/before_install/linux_qt5.sh +++ b/CI/before_install/linux_qt5.sh @@ -8,6 +8,6 @@ sudo apt-get update # - debian build settings at debian/control sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ -qtbase5-dev qttools5-dev \ +qtbase5-dev qttools5-dev libqt5svg5-dev \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies diff --git a/CI/before_install/linux_qt6.sh b/CI/before_install/linux_qt6.sh index 422b50e98..ef83d773d 100644 --- a/CI/before_install/linux_qt6.sh +++ b/CI/before_install/linux_qt6.sh @@ -8,6 +8,6 @@ sudo apt-get update # - debian build settings at debian/control sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ -qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ +qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools qt6-svg-dev libqt6svg6-dev \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies diff --git a/CMakeLists.txt b/CMakeLists.txt index e39744084..e77d6531f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,7 @@ endif() include(CMakeDependentOption) cmake_dependent_option(ENABLE_INNOEXTRACT "Enable innoextract for GOG file extraction in launcher" ON "ENABLE_LAUNCHER" OFF) cmake_dependent_option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON "NOT ENABLE_GOLDMASTER" OFF) +cmake_dependent_option(ENABLE_TEMPLATE_EDITOR "Enable template editor inside map editor" ON "ENABLE_EDITOR" OFF) option(VCMI_PORTMASTER "PortMaster build" OFF) @@ -253,6 +254,10 @@ if(ENABLE_EDITOR) add_definitions(-DENABLE_EDITOR) endif() +if(ENABLE_TEMPLATE_EDITOR) + add_definitions(-DENABLE_TEMPLATE_EDITOR) +endif() + if(ENABLE_SINGLE_APP_BUILD) add_definitions(-DENABLE_SINGLE_APP_BUILD) endif() @@ -521,6 +526,16 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network) + if(ENABLE_TEMPLATE_EDITOR) + find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Svg Xml) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Svg Xml) + + if(QT_VERSION_MAJOR EQUAL 6) + find_package(QT NAMES Qt6 REQUIRED COMPONENTS SvgWidgets) + find_package(Qt6 REQUIRED COMPONENTS SvgWidgets) + endif() + endif() + if(ENABLE_TRANSLATIONS) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS LinguistTools) add_definitions(-DENABLE_QT_TRANSLATIONS) diff --git a/CMakePresets.json b/CMakePresets.json index 5950258a4..624290395 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -189,7 +189,8 @@ ], "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", - "FORCE_BUNDLED_FL": "ON" + "FORCE_BUNDLED_FL": "ON", + "ENABLE_TEMPLATE_EDITOR": "OFF" } }, { @@ -207,7 +208,8 @@ "default-release" ], "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "ENABLE_TEMPLATE_EDITOR": "OFF" } }, { diff --git a/config/schemas/template.json b/config/schemas/template.json index 835bcc720..c2daebbad 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -43,6 +43,10 @@ "terrainTypeLikeZone" : { "type" : "number" }, "treasureLikeZone" : { "type" : "number" }, "customObjectsLikeZone" : { "type" : "number" }, + + "visPositionX" : { "type" : "number" }, + "visPositionY" : { "type" : "number" }, + "visSize" : { "type" : "number" }, "terrainTypes": {"$ref" : "#/definitions/stringArray"}, "bannedTerrains": {"$ref" : "#/definitions/stringArray"}, diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index ad4171ed5..f5c19b771 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -25,7 +25,7 @@ To compile, the following packages (and their development counterparts) are need For Ubuntu and Debian you need to install this list of packages: -`sudo apt-get install cmake g++ clang libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev qtbase5-dev libtbb-dev libluajit-5.1-dev liblzma-dev libsqlite3-dev qttools5-dev ninja-build ccache` +`sudo apt-get install cmake g++ clang libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev qtbase5-dev libqt5svg5-dev libtbb-dev libluajit-5.1-dev liblzma-dev libsqlite3-dev qttools5-dev ninja-build ccache` Alternatively if you have VCMI installed from repository or PPA you can use: diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index e4a6b1b78..1744bc056 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -144,14 +144,17 @@ ZoneOptions::ZoneOptions(): type(ETemplateZoneType::PLAYER_START), size(1), maxTreasureValue(0), - owner(std::nullopt), + owner(PlayerColor(0)), matchTerrainToTown(true), townsAreSameType(false), monsterStrength(EMonsterStrength::ZONE_NORMAL), townsLikeZone(NO_ZONE), minesLikeZone(NO_ZONE), terrainTypeLikeZone(NO_ZONE), - treasureLikeZone(NO_ZONE) + treasureLikeZone(NO_ZONE), + customObjectsLikeZone(NO_ZONE), + visPosition(Point(0, 0)), + visSize(1.0) { } @@ -331,6 +334,26 @@ TRmgTemplateZoneId ZoneOptions::getTownsLikeZone() const return townsLikeZone; } +Point ZoneOptions::getVisPosition() const +{ + return visPosition; +} + +void ZoneOptions::setVisPosition(Point value) +{ + visPosition = value; +} + +float ZoneOptions::getVisSize() const +{ + return visSize; +} + +void ZoneOptions::setVisSize(float value) +{ + visSize = value; +} + void ZoneOptions::addConnection(const ZoneConnection & connection) { connectedZoneIds.push_back(connection.getOtherZoneId(getId())); @@ -496,6 +519,9 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) } handler.serializeStruct("customObjects", objectConfig); + handler.serializeInt("visPositionX", visPosition.x); + handler.serializeInt("visPositionY", visPosition.y); + handler.serializeFloat("visSize", visSize); } ZoneConnection::ZoneConnection(): diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index e7a4b541c..8ad751c9c 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -12,6 +12,7 @@ #include "../int3.h" #include "../GameConstants.h" +#include "../Point.h" #include "../ResourceSet.h" #include "ObjectInfo.h" #include "ObjectConfig.h" @@ -21,6 +22,7 @@ VCMI_LIB_NAMESPACE_BEGIN class JsonSerializeFormat; struct CompoundMapObjectID; +class TemplateEditor; enum class ETemplateZoneType { @@ -93,6 +95,10 @@ enum class ERoadOption class DLL_LINKAGE ZoneConnection { +#ifdef ENABLE_TEMPLATE_EDITOR + friend class ::TemplateEditor; +#endif + public: ZoneConnection(); @@ -120,11 +126,18 @@ private: class DLL_LINKAGE ZoneOptions { +#ifdef ENABLE_TEMPLATE_EDITOR + friend class ::TemplateEditor; +#endif + public: static const TRmgTemplateZoneId NO_ZONE; class DLL_LINKAGE CTownInfo { +#ifdef ENABLE_TEMPLATE_EDITOR + friend class ::TemplateEditor; +#endif public: CTownInfo(); @@ -228,6 +241,12 @@ public: TRmgTemplateZoneId getCustomObjectsLikeZone() const; TRmgTemplateZoneId getTownsLikeZone() const; + Point getVisPosition() const; + void setVisPosition(Point value); + + float getVisSize() const; + void setVisSize(float value); + protected: TRmgTemplateZoneId id; ETemplateZoneType type; @@ -235,6 +254,9 @@ protected: ui32 maxTreasureValue; std::optional owner; + Point visPosition; + float visSize; + ObjectConfig objectConfig; CTownInfo playerTowns; CTownInfo neutralTowns; @@ -268,11 +290,18 @@ protected: /// The CRmgTemplate describes a random map template. class DLL_LINKAGE CRmgTemplate : boost::noncopyable { +#ifdef ENABLE_TEMPLATE_EDITOR + friend class ::TemplateEditor; +#endif + public: using Zones = std::map>; class DLL_LINKAGE CPlayerCountRange { +#ifdef ENABLE_TEMPLATE_EDITOR + friend class ::TemplateEditor; +#endif public: void addRange(int lower, int upper); void addNumber(int value); diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index fcbd0d382..9ef27de05 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -49,6 +49,15 @@ set(editor_SRCS campaigneditor/startingbonus.cpp campaigneditor/campaignview.cpp ) +if(ENABLE_TEMPLATE_EDITOR) + set(editor_SRCS + ${editor_SRCS} + templateeditor/templateeditor.cpp + templateeditor/templateview.cpp + templateeditor/graphicelements.cpp + templateeditor/algorithm.cpp + ) +endif() set(editor_HEADERS StdInc.h @@ -102,6 +111,15 @@ set(editor_HEADERS campaigneditor/startingbonus.h campaigneditor/campaignview.h ) +if(ENABLE_TEMPLATE_EDITOR) + set(editor_HEADERS + ${editor_HEADERS} + templateeditor/templateeditor.h + templateeditor/templateview.h + templateeditor/graphicelements.h + templateeditor/algorithm.h + ) +endif() set(editor_FORMS mainwindow.ui @@ -137,6 +155,12 @@ set(editor_FORMS campaigneditor/scenarioproperties.ui campaigneditor/startingbonus.ui ) +if(ENABLE_TEMPLATE_EDITOR) + set(editor_FORMS + ${editor_FORMS} + templateeditor/templateeditor.ui + ) +endif() set(editor_RESOURCES resources.qrc @@ -254,6 +278,10 @@ if(APPLE) set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor) endif() +if(ENABLE_TEMPLATE_EDITOR) + target_compile_definitions(vcmieditor PRIVATE ENABLE_TEMPLATE_EDITOR) +endif() + # Qt defines 'emit' as macros, which conflicts with TBB definition of method with same name target_compile_definitions(vcmieditor PRIVATE QT_NO_EMIT) @@ -262,6 +290,12 @@ if(ENABLE_STATIC_LIBS OR NOT (ENABLE_EDITOR AND ENABLE_LAUNCHER)) endif() target_link_libraries(vcmieditor vcmi vcmiqt Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +if(ENABLE_TEMPLATE_EDITOR) + target_link_libraries(vcmieditor vcmi vcmiqt Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::Xml) + if(QT_VERSION_MAJOR EQUAL 6) + target_link_libraries(vcmieditor vcmi vcmiqt Qt6::SvgWidgets) + endif() +endif() target_include_directories(vcmieditor PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 97f5147da..b08e2d04c 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -12,6 +12,11 @@ #include "../Global.h" #include +#ifdef ENABLE_TEMPLATE_EDITOR +#include +#include +#include +#endif #include #include #include diff --git a/mapeditor/helper.cpp b/mapeditor/helper.cpp index 62982c4ba..208968a8d 100644 --- a/mapeditor/helper.cpp +++ b/mapeditor/helper.cpp @@ -22,6 +22,10 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/MapFormatJson.h" #include "../lib/modding/ModIncompatibility.h" +#include "../lib/rmg/CRmgTemplate.h" +#include "../lib/serializer/JsonSerializer.h" +#include "../lib/serializer/JsonDeserializer.h" +#include "../lib/serializer/CSaveFile.h" std::unique_ptr Helper::openMapInternal(const QString & filenameSelect) { @@ -77,6 +81,38 @@ std::shared_ptr Helper::openCampaignInternal(const QString & file throw std::runtime_error("Corrupted campaign"); } +std::map> Helper::openTemplateInternal(const QString & filenameSelect) +{ + QFileInfo fi(filenameSelect); + std::string fname = fi.fileName().toStdString(); + std::string fdir = fi.dir().path().toStdString(); + + ResourcePath resId("MAPEDITOR/" + fname, EResType::JSON); + + //addFilesystem takes care about memory deallocation if case of failure, no memory leak here + auto mapEditorFilesystem = std::make_unique("MAPEDITOR/", fdir, 0); + CResourceHandler::removeFilesystem("local", "mapEditor"); + CResourceHandler::addFilesystem("local", "mapEditor", std::move(mapEditorFilesystem)); + + if(!CResourceHandler::get("mapEditor")->existsResource(resId)) + throw std::runtime_error("Cannot open template from this folder"); + + auto data = CResourceHandler::get()->load(resId)->readAll(); + JsonNode nodes(reinterpret_cast(data.first.get()), data.second, resId.getName()); + + std::map> templates; + for(auto & node : nodes.Struct()) + { + JsonDeserializer handler(nullptr, node.second); + auto rmg = std::make_shared(); + rmg->serializeJson(handler); + rmg->validate(); + templates[node.first] = rmg; + } + + return templates; +} + void Helper::saveCampaign(std::shared_ptr campaignState, const QString & filename) { auto jsonCampaign = CampaignHandler::writeHeaderToJson(*campaignState); @@ -107,3 +143,25 @@ void Helper::saveCampaign(std::shared_ptr campaignState, const QS auto jsonCampaignStr = jsonCampaign.toString(); saver->addFile("header.json")->write(reinterpret_cast(jsonCampaignStr.data()), jsonCampaignStr.length()); } + +void Helper::saveTemplate(std::map> tpl, const QString & filename) +{ + JsonMap data; + + for(auto & node : tpl) + { + JsonNode actual; + { + JsonSerializer handler(nullptr, actual); + node.second->serializeJson(handler); + } + data[node.first] = actual; + } + + auto byteData = JsonNode(data).toBytes(); + QByteArray byteDataArray = QByteArray(reinterpret_cast(byteData.data()), static_cast(byteData.size())); + QFile file(filename); + + if(file.open(QIODevice::WriteOnly)) + file.write(byteDataArray); +} diff --git a/mapeditor/helper.h b/mapeditor/helper.h index 90a8b2259..30af17107 100644 --- a/mapeditor/helper.h +++ b/mapeditor/helper.h @@ -12,10 +12,13 @@ class CMap; class CampaignState; +class CRmgTemplate; namespace Helper { std::unique_ptr openMapInternal(const QString &); std::shared_ptr openCampaignInternal(const QString &); + std::map> openTemplateInternal(const QString &); void saveCampaign(std::shared_ptr campaignState, const QString & filename); + void saveTemplate(std::map> tpl, const QString & filename); } \ No newline at end of file diff --git a/mapeditor/icons/dice.png b/mapeditor/icons/dice.png new file mode 100644 index 000000000..b857dc616 Binary files /dev/null and b/mapeditor/icons/dice.png differ diff --git a/mapeditor/icons/templateSquare.svg b/mapeditor/icons/templateSquare.svg new file mode 100644 index 000000000..c8baedca2 --- /dev/null +++ b/mapeditor/icons/templateSquare.svg @@ -0,0 +1,1147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 999 + 99 + 99 + 99 + 99 + 99 + 99 + 99 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 99 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mapeditor/icons/zone-add.svg b/mapeditor/icons/zone-add.svg new file mode 100644 index 000000000..0cdc711db --- /dev/null +++ b/mapeditor/icons/zone-add.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mapeditor/icons/zone-remove.svg b/mapeditor/icons/zone-remove.svg new file mode 100644 index 000000000..e4a2b7681 --- /dev/null +++ b/mapeditor/icons/zone-remove.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mapeditor/icons/zones-layout.svg b/mapeditor/icons/zones-layout.svg new file mode 100644 index 000000000..e4392ed45 --- /dev/null +++ b/mapeditor/icons/zones-layout.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index e437b112b..ff1d9e04a 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -50,6 +50,9 @@ #include "validator.h" #include "helper.h" #include "campaigneditor/campaigneditor.h" +#ifdef ENABLE_TEMPLATE_EDITOR +#include "templateeditor/templateeditor.h" +#endif QJsonValue jsonFromPixmap(const QPixmap &p) { @@ -261,6 +264,11 @@ MainWindow::MainWindow(QWidget* parent) : ui->actionZoom_out->setIcon(QIcon{":/icons/zoom_minus.png"}); ui->actionZoom_reset->setIcon(QIcon{":/icons/zoom_zero.png"}); ui->actionCampaignEditor->setIcon(QIcon{":/icons/mapeditor.64x64.png"}); + ui->actionTemplateEditor->setIcon(QIcon{":/icons/dice.png"}); + +#ifndef ENABLE_TEMPLATE_EDITOR + ui->actionTemplateEditor->setVisible(false); +#endif loadUserSettings(); //For example window size setTitle(); @@ -606,6 +614,17 @@ void MainWindow::on_actionCampaignEditor_triggered() CampaignEditor::showCampaignEditor(); } +void MainWindow::on_actionTemplateEditor_triggered() +{ +#ifdef ENABLE_TEMPLATE_EDITOR + if(!getAnswerAboutUnsavedChanges()) + return; + + hide(); + TemplateEditor::showTemplateEditor(); +#endif +} + void MainWindow::on_actionNew_triggered() { if(getAnswerAboutUnsavedChanges()) diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index fc79f10e6..a734e8843 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -80,6 +80,8 @@ private slots: void on_actionCampaignEditor_triggered(); + void on_actionTemplateEditor_triggered(); + void on_actionNew_triggered(); void on_actionLevel_triggered(); diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 2532e66ae..91f5065e4 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -72,6 +72,7 @@ + @@ -148,6 +149,7 @@ + @@ -1078,6 +1080,14 @@ Campaign editor + + + Template editor + + + Template editor + + View underground diff --git a/mapeditor/resources.qrc b/mapeditor/resources.qrc index e6b28c64c..8dcbcdb59 100644 --- a/mapeditor/resources.qrc +++ b/mapeditor/resources.qrc @@ -7,6 +7,7 @@ icons/document-open.png icons/document-open-recent.png icons/document-save.png + icons/dice.png icons/edit-clear.png icons/edit-copy.png icons/edit-cut.png @@ -42,5 +43,10 @@ icons/zoom_minus.png icons/zoom_plus.png icons/zoom_zero.png + + icons/templateSquare.svg + icons/zone-add.svg + icons/zone-remove.svg + icons/zones-layout.svg diff --git a/mapeditor/templateeditor/algorithm.cpp b/mapeditor/templateeditor/algorithm.cpp new file mode 100644 index 000000000..4f73e1895 --- /dev/null +++ b/mapeditor/templateeditor/algorithm.cpp @@ -0,0 +1,133 @@ +/* + * algorithm.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "algorithm.h" + +double Algorithm::distance(double x1, double y1, double x2, double y2) +{ + return std::sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) + 1e-9; +} + +bool Algorithm::edgesIntersect(const Node& a, const Node& b, const Node& c, const Node& d) +{ + auto cross = [](double x1, double y1, double x2, double y2) { + return x1 * y2 - y1 * x2; + }; + + double dx1 = b.x - a.x, dy1 = b.y - a.y; + double dx2 = d.x - c.x, dy2 = d.y - c.y; + + double delta = cross(dx1, dy1, dx2, dy2); + if (std::abs(delta) < 1e-10) return false; // Parallel + + // Compute intersection + double s = cross(c.x - a.x, c.y - a.y, dx2, dy2) / delta; + double t = cross(c.x - a.x, c.y - a.y, dx1, dy1) / delta; + + return s > 0 && s < 1 && t > 0 && t < 1; +} + +void Algorithm::forceDirectedLayout(std::vector & nodes, const std::vector & edges, int iterations, double width, double height) +{ + const double area = width * height; + const double k = std::sqrt(area / nodes.size()); + + for (int it = 0; it < iterations; ++it) + { + // Reset forces + for (auto& node : nodes) + node.dx = node.dy = 0; + + // Repulsive forces + for (size_t i = 0; i < nodes.size(); ++i) + { + for (size_t j = i + 1; j < nodes.size(); ++j) + { + double dx = nodes[i].x - nodes[j].x; + double dy = nodes[i].y - nodes[j].y; + double dist = distance(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); + double force = (k * k) / dist; + + nodes[i].dx += (dx / dist) * force; + nodes[i].dy += (dy / dist) * force; + nodes[j].dx -= (dx / dist) * force; + nodes[j].dy -= (dy / dist) * force; + } + } + + // Attractive forces + for (const auto& edge : edges) + { + Node& u = nodes[edge.from]; + Node& v = nodes[edge.to]; + double dx = u.x - v.x; + double dy = u.y - v.y; + double dist = distance(u.x, u.y, v.x, v.y); + double force = (dist * dist) / k; + + double fx = (dx / dist) * force; + double fy = (dy / dist) * force; + + u.dx -= fx; + u.dy -= fy; + v.dx += fx; + v.dy += fy; + } + + // Edge crossing penalty + for (size_t i = 0; i < edges.size(); ++i) { + for (size_t j = i + 1; j < edges.size(); ++j) { + const Edge& e1 = edges[i]; + const Edge& e2 = edges[j]; + + if (e1.from == e2.from || e1.from == e2.to || + e1.to == e2.from || e1.to == e2.to) + continue; // Skip if they share nodes + + Node& a = nodes[e1.from]; + Node& b = nodes[e1.to]; + Node& c = nodes[e2.from]; + Node& d = nodes[e2.to]; + + if (edgesIntersect(a, b, c, d)) { + double strength = 0.05; + + a.dx += strength * (a.x - c.x); + a.dy += strength * (a.y - c.y); + b.dx += strength * (b.x - d.x); + b.dy += strength * (b.y - d.y); + c.dx += strength * (c.x - a.x); + c.dy += strength * (c.y - a.y); + d.dx += strength * (d.x - b.x); + d.dy += strength * (d.y - b.y); + } + } + } + + // Apply displacement + for (auto& node : nodes) + { + node.x += std::max(-5.0, std::min(5.0, node.dx)); + node.y += std::max(-5.0, std::min(5.0, node.dy)); + + // Keep within bounds + node.x = std::min(width, std::max(0.0, node.x)); + node.y = std::min(height, std::max(0.0, node.y)); + } + } + + for (auto& node : nodes) + { + // Center around 0 + node.x -= width / 2; + node.y -= height / 2; + } +} diff --git a/mapeditor/templateeditor/algorithm.h b/mapeditor/templateeditor/algorithm.h new file mode 100644 index 000000000..aed4131ca --- /dev/null +++ b/mapeditor/templateeditor/algorithm.h @@ -0,0 +1,32 @@ +/* + * algorithm.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../StdInc.h" + +class Algorithm +{ +public: + struct Node + { + double x, y; + double dx = 0, dy = 0; + int id; + }; + + struct Edge + { + int from, to; + }; + + static double distance(double x1, double y1, double x2, double y2); + static bool edgesIntersect(const Node& a, const Node& b, const Node& c, const Node& d); + static void forceDirectedLayout(std::vector& nodes, const std::vector& edges, int iterations, double width, double height); +}; diff --git a/mapeditor/templateeditor/graphicelements.cpp b/mapeditor/templateeditor/graphicelements.cpp new file mode 100644 index 000000000..547872f67 --- /dev/null +++ b/mapeditor/templateeditor/graphicelements.cpp @@ -0,0 +1,292 @@ +/* + * graphicelements.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "graphicelements.h" + +#include + +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/rmg/CRmgTemplate.h" + +QDomElement CardItem::getElementById(const QDomDocument& doc, const QString& id) +{ + QDomElement root = doc.documentElement(); + + std::function findById = [&](const QDomElement& elem) -> QDomElement { + if (elem.attribute("id") == id) + return elem; + + QDomElement child = elem.firstChildElement(); + while (!child.isNull()) + { + QDomElement found = findById(child); + if (!found.isNull()) + return found; + child = child.nextSiblingElement(); + } + return QDomElement(); + }; + + return findById(root); +} + +bool isBlackTextNeeded(const QColor& bg) +{ + int r = bg.red(); + int g = bg.green(); + int b = bg.blue(); + + double luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.5; +} + +CardItem::CardItem(): + selectCallback(nullptr), + posChangeCallback(nullptr), + useBlackText(false), + mousePressed(false) +{ + QFile file(":/icons/templateSquare.svg"); + file.open(QIODevice::ReadOnly); + QByteArray data = file.readAll(); + doc.setContent(data); + + updateContent(); +} + +void CardItem::setSelectCallback(std::function func) +{ + selectCallback = func; +} + +void CardItem::setPosChangeCallback(std::function func) +{ + posChangeCallback = func; +} + +void CardItem::updateContent() +{ + setSharedRenderer(new QSvgRenderer(doc.toByteArray())); +} + +void CardItem::setFillColor(QColor color) +{ + auto squareElem = getElementById(doc, "rect"); + squareElem.setAttribute("style", squareElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:" + color.name() + ";")); + + useBlackText = isBlackTextNeeded(color); +} + +void CardItem::setMultiFillColor(QColor color1, QColor color2) +{ + auto squareElem = getElementById(doc, "rect"); + squareElem.setAttribute("style", squareElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:url(#gradientExtra);")); + auto gradientStopElem1 = getElementById(doc, "gradientExtraColorStop1"); + auto gradientStopElem2 = getElementById(doc, "gradientExtraColorStop2"); + auto gradientStopElem3 = getElementById(doc, "gradientExtraColorStop3"); + auto gradientStopElem4 = getElementById(doc, "gradientExtraColorStop4"); + auto gradientStopElem5 = getElementById(doc, "gradientExtraColorStop5"); + gradientStopElem1.setAttribute("style", gradientStopElem1.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + color1.name() + ";")); + gradientStopElem2.setAttribute("style", gradientStopElem2.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + color2.name() + ";")); + gradientStopElem3.setAttribute("style", gradientStopElem3.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + color1.name() + ";")); + gradientStopElem4.setAttribute("style", gradientStopElem4.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + color2.name() + ";")); + gradientStopElem5.setAttribute("style", gradientStopElem5.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + color1.name() + ";")); + + useBlackText = isBlackTextNeeded(color1); +} + +void CardItem::setPlayerColor(PlayerColor color) +{ + std::map> colors = + { + { PlayerColor(0), { "#F80000", "#920000" } }, //red + { PlayerColor(1), { "#0000F8", "#000092" } }, //blue + { PlayerColor(2), { "#9B7251", "#35271C" } }, //tan + { PlayerColor(3), { "#00FC00", "#009600" } }, //green + { PlayerColor(4), { "#F88000", "#924B00" } }, //orange + { PlayerColor(5), { "#F800F8", "#920092" } }, //purple + { PlayerColor(6), { "#00FCF8", "#009694" } }, //teal + { PlayerColor(7), { "#C07888", "#5A3840" } }, //pink + }; + auto squareElem = getElementById(doc, "rect"); + squareElem.setAttribute("style", squareElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:url(#gradientPlayer);")); + auto gradientStopElem1 = getElementById(doc, "gradientPlayerColorStop1"); + auto gradientStopElem2 = getElementById(doc, "gradientPlayerColorStop2"); + gradientStopElem1.setAttribute("style", gradientStopElem1.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + colors[color].first + ";")); + gradientStopElem2.setAttribute("style", gradientStopElem2.attribute("style").replace(QRegularExpression("stop-color:.*?;"), "stop-color:" + colors[color].second + ";")); + + useBlackText = isBlackTextNeeded(QColor(colors[color].first)); +} + +void CardItem::setJunction(bool val) +{ + auto squareElem = getElementById(doc, "rectJunction"); + squareElem.setAttribute("style", squareElem.attribute("style").replace(QRegularExpression("stroke-opacity:.*?;"), "stroke-opacity:" + QString::fromStdString(val ? "0.3" : "0.0") + ";")); +} + +void CardItem::setId(int val) +{ + auto textIdElem = getElementById(doc, "textId"); + textIdElem.setAttribute("style", textIdElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:" + QColor(useBlackText ? Qt::black : Qt::white).name() + ";")); + textIdElem.firstChild().setNodeValue(QString::number(val)); + + id = val; +} + +int CardItem::getId() +{ + return id; +} + +void CardItem::setResAmount(GameResID res, int val) +{ + std::map names = + { + { GameResID::WOOD, "Wood" }, + { GameResID::ORE, "Ore" }, + { GameResID::MERCURY, "Mercury" }, + { GameResID::SULFUR, "Sulfur" }, + { GameResID::CRYSTAL, "Crystal" }, + { GameResID::GEMS, "Gems" }, + { GameResID::GOLD, "Gold" }, + }; + auto textElem = getElementById(doc, "text" + names[res]); + textElem.setAttribute("style", textElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:" + QColor(useBlackText ? Qt::black : Qt::white).name() + ";")); + textElem.firstChild().setNodeValue(val ? QString::number(val) : ""); + + auto iconElem = getElementById(doc, "icon" + names[res]); + iconElem.setAttribute("opacity", val ? "1.0" : "0.1"); +} + +void CardItem::setChestValue(int val) +{ + auto textElem = getElementById(doc, "textChest"); + textElem.setAttribute("style", textElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:" + QColor(useBlackText ? Qt::black : Qt::white).name() + ";")); + textElem.firstChild().setNodeValue(val ? QString::number(val) : ""); + + auto iconElem = getElementById(doc, "iconChest"); + iconElem.setAttribute("opacity", val ? "1.0" : "0.1"); +} + +void CardItem::setSword(EMonsterStrength::EMonsterStrength val) +{ + int level = 0; + if(val == EMonsterStrength::ZONE_WEAK || val == EMonsterStrength::GLOBAL_WEAK) + level = 1; + else if(val == EMonsterStrength::ZONE_NORMAL || val == EMonsterStrength::GLOBAL_NORMAL) + level = 2; + else if(val == EMonsterStrength::ZONE_STRONG || val == EMonsterStrength::GLOBAL_STRONG) + level = 3; + + getElementById(doc, "iconSword1").setAttribute("opacity", level > 0 ? "1.0" : "0.1"); + getElementById(doc, "iconSword2").setAttribute("opacity", level > 1 ? "1.0" : "0.1"); + getElementById(doc, "iconSword3").setAttribute("opacity", level > 2 ? "1.0" : "0.1"); +} + +void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if(event->button() == Qt::LeftButton) + { + // set element in grid + double xx = x() + (boundingRect().width() / 2); + double yy = y() + (boundingRect().height() / 2); + xx = GRID_SIZE * round(xx / GRID_SIZE); + yy = GRID_SIZE * round(yy / GRID_SIZE); + setPos(xx - (boundingRect().width() / 2), yy - (boundingRect().height() / 2)); + } + + QGraphicsSvgItem::mouseReleaseEvent(event); + + if(posChangeCallback) + posChangeCallback(pos()); + + mousePressed = false; +} + +void CardItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsSvgItem::mousePressEvent(event); + + mousePressed = true; +} + +QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if(change == ItemSelectedHasChanged && selectCallback) + selectCallback(isSelected()); + else if(change == ItemPositionHasChanged && posChangeCallback && mousePressed) + posChangeCallback(pos()); + + return QGraphicsSvgItem::itemChange(change, value); +} + +LineItem::LineItem(): + clickCallback(nullptr) +{ + setZValue(-2); + for(int i = 0; i < 10; i++) // render multiple times to increase outline effect + { + auto tmpTextItem = new QGraphicsTextItem(this); + tmpTextItem->setZValue(-1); + QFont font; + font.setPointSize(18); + tmpTextItem->setFont(font); + QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(); + shadowEffect->setBlurRadius(10); + shadowEffect->setEnabled(true); + shadowEffect->setOffset(0, 0); + shadowEffect->setColor(Qt::black); + tmpTextItem->setGraphicsEffect(shadowEffect); + tmpTextItem->setDefaultTextColor(Qt::white); + textItem.push_back(tmpTextItem); + } +} + +void LineItem::setLineToolTip(const QString &toolTip) +{ + for(auto & tmpTextItem : textItem) + tmpTextItem->setToolTip(toolTip); + setToolTip(toolTip); +} + +void LineItem::setClickCallback(std::function func) +{ + clickCallback = func; +} + +void LineItem::setText(QString text) +{ + for(auto & tmpTextItem : textItem) + { + tmpTextItem->setPlainText(text); + QRectF lineRect = boundingRect(); + QRectF textRect = tmpTextItem->boundingRect(); + tmpTextItem->setPos(QPointF(lineRect.x() + (lineRect.width() / 2) - (textRect.width() / 2), lineRect.y() + (lineRect.height() / 2) - (textRect.height() / 2))); + } +} + +void LineItem::setId(int val) +{ + id = val; +} + +int LineItem::getId() +{ + return id; +} + +void LineItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if(event->button() == Qt::LeftButton && clickCallback) + clickCallback(); + + QGraphicsLineItem::mousePressEvent(event); +} \ No newline at end of file diff --git a/mapeditor/templateeditor/graphicelements.h b/mapeditor/templateeditor/graphicelements.h new file mode 100644 index 000000000..465992e60 --- /dev/null +++ b/mapeditor/templateeditor/graphicelements.h @@ -0,0 +1,73 @@ +/* + * graphicelements.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once +#include +#include + +#include "../StdInc.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/rmg/CRmgTemplate.h" + +class CardItem : public QGraphicsSvgItem +{ +private: + QDomElement getElementById(const QDomDocument& doc, const QString& id); + + QDomDocument doc; + bool useBlackText; + int id = -1; + bool mousePressed; + + std::function selectCallback; + std::function posChangeCallback; +public: + CardItem(); + + static constexpr double GRID_SIZE = 10.; + + void setSelectCallback(std::function func); + void setPosChangeCallback(std::function func); + + void updateContent(); + void setFillColor(QColor color); + void setMultiFillColor(QColor color1, QColor color2); + void setPlayerColor(PlayerColor color); + void setJunction(bool val); + void setId(int val); + int getId(); + void setResAmount(GameResID res, int val); + void setChestValue(int val); + void setSword(EMonsterStrength::EMonsterStrength val); + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; +}; + +class LineItem : public QGraphicsLineItem +{ +private: + std::vector textItem; + std::function clickCallback; + int id = -1; + + static constexpr int CLICKABLE_PADDING_AROUND_LINE = 10; +public: + LineItem(); + void setClickCallback(std::function func); + void setText(QString text); + void setId(int val); + int getId(); + void setLineToolTip(const QString &toolTip); + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; +}; diff --git a/mapeditor/templateeditor/templateeditor.cpp b/mapeditor/templateeditor/templateeditor.cpp new file mode 100644 index 000000000..4e16da98f --- /dev/null +++ b/mapeditor/templateeditor/templateeditor.cpp @@ -0,0 +1,1070 @@ +/* + * templateeditor.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "templateeditor.h" +#include "ui_templateeditor.h" +#include "graphicelements.h" +#include "algorithm.h" + +#include "../helper.h" + +#include "../../lib/VCMIDirs.h" +#include "../../lib/rmg/CRmgTemplate.h" +#include "../../lib/texts/MetaString.h" + +TemplateEditor::TemplateEditor(): + ui(new Ui::TemplateEditor) +{ + ui->setupUi(this); + + setWindowIcon(QIcon{":/icons/menu-game.png"}); + ui->actionOpen->setIcon(QIcon{":/icons/document-open.png"}); + ui->actionSave->setIcon(QIcon{":/icons/document-save.png"}); + ui->actionNew->setIcon(QIcon{":/icons/document-new.png"}); + ui->actionAddZone->setIcon(QIcon{":/icons/zone-add.svg"}); + ui->actionRemoveZone->setIcon(QIcon{":/icons/zone-remove.svg"}); + ui->actionAutoPosition->setIcon(QIcon{":/icons/zones-layout.svg"}); + ui->actionZoom_in->setIcon(QIcon{":/icons/zoom_plus.png"}); + ui->actionZoom_out->setIcon(QIcon{":/icons/zoom_minus.png"}); + ui->actionZoom_auto->setIcon(QIcon{":/icons/zoom_base.png"}); + ui->actionZoom_reset->setIcon(QIcon{":/icons/zoom_zero.png"}); + + templateScene.reset(new TemplateScene()); + ui->templateView->setScene(templateScene.get()); + + ui->toolBox->setVisible(false); + + ui->spinBoxZoneVisPosX->setSingleStep(CardItem::GRID_SIZE); + ui->spinBoxZoneVisPosY->setSingleStep(CardItem::GRID_SIZE); + + ui->templateView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->templateView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->templateView->setSceneRect(-3000, -3000, 6000, 6000); + ui->templateView->centerOn(QPointF(0, 0)); + ui->templateView->setDragMode(QGraphicsView::DragMode::ScrollHandDrag); + + loadContent(); + + setTitle(); + + setWindowModality(Qt::ApplicationModal); + + show(); +} + +TemplateEditor::~TemplateEditor() +{ + delete ui; +} + +void TemplateEditor::initContent() +{ + ui->comboBoxTemplateSelection->clear(); + + ui->toolBox->setVisible(false); + ui->pageZone->setEnabled(false); + + if(templates.empty()) + return; + + ui->toolBox->setVisible(true); + + selectedTemplate = templates.begin()->first; + for(auto & tpl : templates) + ui->comboBoxTemplateSelection->addItem(QString::fromStdString(tpl.first)); +} + +void TemplateEditor::autoPositionZones() +{ + auto & zones = templates[selectedTemplate]->getZones(); + + std::vector nodes; + std::default_random_engine rng(0); + std::uniform_real_distribution distX(0.0, 500); + std::uniform_real_distribution distY(0.0, 500); + for(auto & item : zones) + { + Algorithm::Node node; + node.x = distX(rng); + node.y = distY(rng); + node.id = item.first; + nodes.push_back(node); + } + std::vector edges; + for(auto & item : templates[selectedTemplate]->getConnectedZoneIds()) + edges.push_back({ + vstd::find_pos_if(nodes, [item](auto & elem){ return elem.id == item.getZoneA(); }), + vstd::find_pos_if(nodes, [item](auto & elem){ return elem.id == item.getZoneB(); }) + }); + + Algorithm::forceDirectedLayout(nodes, edges, 1000, 500, 500); + + for(auto & item : nodes) + zones.at(item.id)->setVisPosition(Point(CardItem::GRID_SIZE * round(item.x / CardItem::GRID_SIZE), CardItem::GRID_SIZE * round(item.y / CardItem::GRID_SIZE))); +} + +void TemplateEditor::loadContent(bool autoPosition) +{ + cards.clear(); + lines.clear(); + + selectedZone = -1; + + templateScene->clear(); + + if(templates.empty()) + return; + + auto & zones = templates[selectedTemplate]->getZones(); + if(autoPosition || std::all_of(zones.begin(), zones.end(), [](auto & item){ return item.second->getVisPosition().x == 0 && item.second->getVisPosition().y == 0; })) + autoPositionZones(); + + for(auto & zone : zones) + if(zone.second->getVisSize() < 0.01) + zone.second->setVisSize(1.0); + + for(auto & zone : zones) + { + auto svgItem = new CardItem(); + svgItem->setSelectCallback([this, svgItem](bool selected){ + ui->pageZone->setDisabled(!selected); + ui->toolBox->setCurrentIndex(selected ? 1 : 0); + ui->actionRemoveZone->setEnabled(selected); + if(selected) + { + selectedZone = svgItem->getId(); + loadZoneMenuContent(); + } + else + selectedZone = -1; + }); + svgItem->setPosChangeCallback([this, svgItem](QPointF pos){ + updateConnectionLines(); + for(auto & zone : templates[selectedTemplate]->getZones()) + if(zone.first == svgItem->getId()) + { + zone.second->setVisPosition(Point(svgItem->pos().toPoint().rx() + (svgItem->boundingRect().width() * svgItem->scale() / 2), svgItem->pos().toPoint().ry() + (svgItem->boundingRect().height() * svgItem->scale() / 2))); + zone.second->setVisSize(svgItem->scale()); + } + loadZoneMenuContent(true); + }); + svgItem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); + + templateScene->addItem(svgItem); + cards[zone.first] = svgItem; + + updateZoneCards(); + } + + updateConnectionLines(true); + updateZonePositions(); + + ui->templateView->show(); + + ui->toolBox->setCurrentIndex(0); + connect(ui->toolBox, &QToolBox::currentChanged, this, [this](int index) mutable { + if(!ui->pageZone->isEnabled() && index == 1) + ui->toolBox->setCurrentIndex(0); + }); + + ui->lineEditName->setText(QString::fromStdString(templates[selectedTemplate]->getName())); + ui->lineEditDescription->setText(QString::fromStdString(templates[selectedTemplate]->getDescription())); + ui->spinBoxMinSizeX->setValue(templates[selectedTemplate]->getMapSizes().first.x); + ui->spinBoxMinSizeY->setValue(templates[selectedTemplate]->getMapSizes().first.y); + ui->spinBoxMinSizeZ->setValue(templates[selectedTemplate]->getMapSizes().first.z); + ui->spinBoxMaxSizeX->setValue(templates[selectedTemplate]->getMapSizes().second.x); + ui->spinBoxMaxSizeY->setValue(templates[selectedTemplate]->getMapSizes().second.y); + ui->spinBoxMaxSizeZ->setValue(templates[selectedTemplate]->getMapSizes().second.z); + ui->checkBoxAllowedWaterContentNone->setChecked(templates[selectedTemplate]->allowedWaterContent.count(EWaterContent::EWaterContent::NONE)); + ui->checkBoxAllowedWaterContentNormal->setChecked(templates[selectedTemplate]->allowedWaterContent.count(EWaterContent::EWaterContent::NORMAL)); + ui->checkBoxAllowedWaterContentIslands->setChecked(templates[selectedTemplate]->allowedWaterContent.count(EWaterContent::EWaterContent::ISLANDS)); + + auto setPlayersTable = [this](QTableWidget * widget, std::vector> & range){ + widget->clear(); + widget->clearContents(); + widget->setRowCount(0); + widget->setColumnCount(3); + widget->setHorizontalHeaderLabels({ tr("Min"), tr("Max"), tr("Action") }); + widget->horizontalHeader()->setStretchLastSection(true); + for(int i = 0; i < range.size(); i++) + { + widget->insertRow(i); + QSpinBox* minSpin = new QSpinBox(); + QSpinBox* maxSpin = new QSpinBox(); + QPushButton* delButton = new QPushButton(); + minSpin->setRange(0, 999); + maxSpin->setRange(0, 999); + minSpin->setValue(range.at(i).first); + maxSpin->setValue(range.at(i).second); + connect(minSpin, static_cast(&QSpinBox::valueChanged), this, [i, &range](int val){ + range.at(i).first = val; + }); + connect(maxSpin, static_cast(&QSpinBox::valueChanged), this, [i, &range](int val){ + range.at(i).second = val; + }); + delButton->setText(tr("Delete")); + connect(delButton, &QPushButton::clicked, this, [this, i, &range](){ + range.erase(range.begin() + i); + loadContent(); + }); + widget->setCellWidget(i, 0, minSpin); + widget->setCellWidget(i, 1, maxSpin); + widget->setCellWidget(i, 2, delButton); + } + widget->insertRow(widget->rowCount()); + QPushButton* addButton = new QPushButton(); + addButton->setText(tr("Add")); + connect(addButton, &QPushButton::clicked, this, [this, &range](){ + range.push_back(std::make_pair(0, 0)); + loadContent(); + }); + widget->setCellWidget(widget->rowCount() - 1, 2, addButton); + }; + setPlayersTable(ui->tableWidgetPlayersPlayer, templates[selectedTemplate]->players.range); + setPlayersTable(ui->tableWidgetPlayersHuman, templates[selectedTemplate]->humanPlayers.range); + + loadZoneConnectionMenuContent(); +} + +void TemplateEditor::updateZonePositions() +{ + for(auto & card : cards) + { + auto & zone = templates[selectedTemplate]->getZones().at(card.first); + card.second->setPos(zone->getVisPosition().x - (card.second->boundingRect().width() * card.second->scale() / 2), zone->getVisPosition().y - (card.second->boundingRect().height() * card.second->scale() / 2)); + card.second->setScale(zone->getVisSize()); + } + + updateConnectionLines(); +} + +QString TemplateEditor::getZoneToolTip(std::shared_ptr zone) +{ + QString tmp{}; + tmp.append(tr("ID: %1").arg(QString::number(zone->getId()))); + tmp.append("\n"); + tmp.append(tr("Max treasure: %1").arg(QString::number(zone->getMaxTreasureValue()))); + //TODO: extend other interesting things (terrains, towns, ...) + return tmp; +} + +void TemplateEditor::updateZoneCards(TRmgTemplateZoneId id) +{ + for(auto & card : cards) + { + if(card.first != id && id > -1) + continue; + + auto & zone = templates[selectedTemplate]->getZones().at(card.first); + auto type = zone->getType(); + + if(type == ETemplateZoneType::PLAYER_START || type == ETemplateZoneType::CPU_START) + card.second->setPlayerColor(PlayerColor(*zone->getOwner())); + else if(type == ETemplateZoneType::TREASURE) + card.second->setMultiFillColor(QColor(165, 125, 55), QColor(250, 229, 157)); + else if(type == ETemplateZoneType::WATER) + card.second->setMultiFillColor(QColor(55, 68, 165), QColor(157, 250, 247)); + else + card.second->setFillColor(QColor(200, 200, 200)); + card.second->setJunction(type == ETemplateZoneType::JUNCTION); + + card.second->setId(card.first); + for(auto & res : {GameResID::WOOD, GameResID::ORE, GameResID::MERCURY, GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS, GameResID::GOLD}) + card.second->setResAmount(res, zone->getMinesInfo().count(res) ? zone->getMinesInfo().at(res) : 0); + card.second->setChestValue(zone->getMaxTreasureValue()); + card.second->setSword(zone->monsterStrength); + card.second->setToolTip(getZoneToolTip(zone)); + card.second->setScale(zone->getVisSize()); + card.second->updateContent(); + } +} + +void TemplateEditor::loadZoneMenuContent(bool onlyPosition) +{ + if(selectedZone < 0 || selectedTemplate.empty()) + return; + + auto setValue = [](auto& target, const auto& newValue){ target->setValue(newValue); }; + auto & zone = templates[selectedTemplate]->getZones().at(selectedZone); + setValue(ui->spinBoxZoneVisPosX, zone->getVisPosition().x); + setValue(ui->spinBoxZoneVisPosY, zone->getVisPosition().y); + setValue(ui->doubleSpinBoxZoneVisSize, zone->getVisSize()); + + if(onlyPosition) + return; + + setValue(ui->spinBoxZoneSize, zone->getSize()); + setValue(ui->spinBoxTownCountPlayer, zone->playerTowns.townCount); + setValue(ui->spinBoxCastleCountPlayer, zone->playerTowns.castleCount); + setValue(ui->spinBoxTownDensityPlayer, zone->playerTowns.townDensity); + setValue(ui->spinBoxCastleDensityPlayer, zone->playerTowns.castleDensity); + setValue(ui->spinBoxTownCountNeutral, zone->neutralTowns.townCount); + setValue(ui->spinBoxCastleCountNeutral, zone->neutralTowns.castleCount); + setValue(ui->spinBoxTownDensityNeutral, zone->neutralTowns.townDensity); + setValue(ui->spinBoxCastleDensityNeutral, zone->neutralTowns.castleDensity); + ui->checkBoxMatchTerrainToTown->setChecked(zone->matchTerrainToTown); + ui->checkBoxTownsAreSameType->setChecked(zone->townsAreSameType); + ui->checkBoxZoneLinkTowns->setChecked(zone->townsLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->checkBoxZoneLinkMines->setChecked(zone->minesLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->checkBoxZoneLinkTerrain->setChecked(zone->terrainTypeLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->checkBoxZoneLinkTreasure->setChecked(zone->treasureLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->checkBoxZoneLinkCustomObjects->setChecked(zone->customObjectsLikeZone != rmg::ZoneOptions::NO_ZONE); + setValue(ui->spinBoxZoneLinkTowns, zone->townsLikeZone != rmg::ZoneOptions::NO_ZONE ? zone->townsLikeZone : 0); + setValue(ui->spinBoxZoneLinkMines, zone->minesLikeZone != rmg::ZoneOptions::NO_ZONE ? zone->minesLikeZone : 0); + setValue(ui->spinBoxZoneLinkTerrain, zone->terrainTypeLikeZone != rmg::ZoneOptions::NO_ZONE ? zone->terrainTypeLikeZone : 0); + setValue(ui->spinBoxZoneLinkTreasure, zone->treasureLikeZone != rmg::ZoneOptions::NO_ZONE ? zone->treasureLikeZone : 0); + setValue(ui->spinBoxZoneLinkCustomObjects, zone->customObjectsLikeZone != rmg::ZoneOptions::NO_ZONE ? zone->customObjectsLikeZone : 0); + ui->spinBoxZoneLinkTowns->setEnabled(zone->townsLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->spinBoxZoneLinkMines->setEnabled(zone->minesLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->spinBoxZoneLinkTerrain->setEnabled(zone->terrainTypeLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->spinBoxZoneLinkTreasure->setEnabled(zone->treasureLikeZone != rmg::ZoneOptions::NO_ZONE); + ui->spinBoxZoneLinkCustomObjects->setEnabled(zone->customObjectsLikeZone != rmg::ZoneOptions::NO_ZONE); + + setValue(ui->spinBoxZoneId, zone->id); + ui->spinBoxZoneId->setEnabled(false); + + ui->comboBoxZoneType->clear(); + ui->comboBoxZoneType->addItem(tr("Player start"), QVariant(static_cast(ETemplateZoneType::PLAYER_START))); + ui->comboBoxZoneType->addItem(tr("CPU start"), QVariant(static_cast(ETemplateZoneType::CPU_START))); + ui->comboBoxZoneType->addItem(tr("Treasure"), QVariant(static_cast(ETemplateZoneType::TREASURE))); + ui->comboBoxZoneType->addItem(tr("Junction"), QVariant(static_cast(ETemplateZoneType::JUNCTION))); + ui->comboBoxZoneType->addItem(tr("Water"), QVariant(static_cast(ETemplateZoneType::WATER))); + ui->comboBoxZoneType->addItem(tr("Sealed"), QVariant(static_cast(ETemplateZoneType::SEALED))); + for (int i = 0; i < ui->comboBoxZoneType->count(); ++i) + if (ui->comboBoxZoneType->itemData(i).toInt() == static_cast(zone->getType())) + ui->comboBoxZoneType->setCurrentIndex(i); + + ui->comboBoxZoneOwner->clear(); + auto type = static_cast(ui->comboBoxZoneType->currentData().toInt()); + if((type == ETemplateZoneType::PLAYER_START || type == ETemplateZoneType::CPU_START)) + { + ui->comboBoxZoneOwner->setEnabled(true); + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + MetaString str; + str.appendName(color); + ui->comboBoxZoneOwner->addItem(QString::fromStdString(str.toString()), QVariant(static_cast(color))); + } + for (int i = 0; i < ui->comboBoxZoneOwner->count(); ++i) + if (ui->comboBoxZoneOwner->itemData(i).toInt() == static_cast(*zone->getOwner())) + ui->comboBoxZoneOwner->setCurrentIndex(i); + } + else + { + ui->comboBoxZoneOwner->addItem(tr("None")); + ui->comboBoxZoneOwner->setEnabled(false); + } + + ui->comboBoxMonsterStrength->clear(); + ui->comboBoxMonsterStrength->addItem(tr("None"), QVariant(static_cast(EMonsterStrength::EMonsterStrength::ZONE_NONE))); + ui->comboBoxMonsterStrength->addItem(tr("Random"), QVariant(static_cast(EMonsterStrength::EMonsterStrength::RANDOM))); + ui->comboBoxMonsterStrength->addItem(tr("Weak"), QVariant(static_cast(EMonsterStrength::EMonsterStrength::ZONE_WEAK))); + ui->comboBoxMonsterStrength->addItem(tr("Normal"), QVariant(static_cast(EMonsterStrength::EMonsterStrength::ZONE_NORMAL))); + ui->comboBoxMonsterStrength->addItem(tr("Strong"), QVariant(static_cast(EMonsterStrength::EMonsterStrength::ZONE_STRONG))); + for (int i = 0; i < ui->comboBoxMonsterStrength->count(); ++i) + if (ui->comboBoxMonsterStrength->itemData(i).toInt() == static_cast(zone->monsterStrength)) + ui->comboBoxMonsterStrength->setCurrentIndex(i); +} + +void TemplateEditor::loadZoneConnectionMenuContent() +{ + auto widget = ui->tableWidgetConnections; + auto & connections = templates[selectedTemplate]->connections; + + widget->clear(); + widget->clearContents(); + widget->setRowCount(0); + widget->setColumnCount(6); + widget->setHorizontalHeaderLabels({ tr("Zone A"), tr("Zone B"), tr("Guard"), tr("Type"), tr("Road"), "" }); + for (int i = 0; i < connections.size(); i++) + { + int row = widget->rowCount(); + widget->insertRow(row); + QSpinBox* zoneA = new QSpinBox(); + QSpinBox* zoneB = new QSpinBox(); + QSpinBox* guardStrength = new QSpinBox(); + QComboBox* connectionType = new QComboBox(); + QComboBox* hasRoad = new QComboBox(); + QPushButton* delButton = new QPushButton(); + zoneA->setRange(0, 999); + zoneB->setRange(0, 999); + guardStrength->setRange(0, 99999); + connectionType->addItem(tr("Guarded"), QVariant(static_cast(rmg::EConnectionType::GUARDED))); + connectionType->addItem(tr("Fictive"), QVariant(static_cast(rmg::EConnectionType::FICTIVE))); + connectionType->addItem(tr("Repulsive"), QVariant(static_cast(rmg::EConnectionType::REPULSIVE))); + connectionType->addItem(tr("Wide"), QVariant(static_cast(rmg::EConnectionType::WIDE))); + connectionType->addItem(tr("Force portal"), QVariant(static_cast(rmg::EConnectionType::FORCE_PORTAL))); + hasRoad->addItem(tr("Random"), QVariant(static_cast(rmg::ERoadOption::ROAD_RANDOM))); + hasRoad->addItem(tr("Yes"), QVariant(static_cast(rmg::ERoadOption::ROAD_TRUE))); + hasRoad->addItem(tr("No"), QVariant(static_cast(rmg::ERoadOption::ROAD_FALSE))); + zoneA->setValue(connections.at(i).getZoneA()); + zoneB->setValue(connections.at(i).getZoneB()); + guardStrength->setValue(connections.at(i).getGuardStrength()); + for (int j = 0; j < connectionType->count(); ++j) + if (connectionType->itemData(j).toInt() == static_cast(connections[i].getConnectionType())) + connectionType->setCurrentIndex(j); + for (int j = 0; j < hasRoad->count(); ++j) + if (hasRoad->itemData(j).toInt() == static_cast(connections[i].hasRoad)) + hasRoad->setCurrentIndex(j); + delButton->setText(tr("Del")); + delButton->setToolTip(tr("Delete")); + connect(zoneA, static_cast(&QSpinBox::valueChanged), this, [this, i, &connections](int val){ + connections.at(i).zoneA = val; + updateConnectionLines(true); + changed(); + }); + connect(zoneB, static_cast(&QSpinBox::valueChanged), this, [this, i, &connections](int val){ + connections.at(i).zoneB = val; + updateConnectionLines(true); + changed(); + }); + connect(guardStrength, static_cast(&QSpinBox::valueChanged), this, [this, i, &connections](int val){ + connections.at(i).guardStrength = val; + updateConnectionLines(); + changed(); + }); + connect(connectionType, QOverload::of(&QComboBox::currentIndexChanged), this, [this, i, &connections, connectionType](int val){ + connections.at(i).connectionType = static_cast(connectionType->itemData(val).toInt()); + updateConnectionLines(); + changed(); + }); + connect(hasRoad, QOverload::of(&QComboBox::currentIndexChanged), this, [this, i, &connections, hasRoad](int val){ + connections.at(i).hasRoad = static_cast(hasRoad->itemData(val).toInt()); + updateConnectionLines(); + changed(); + }); + connect(delButton, &QPushButton::pressed, this, [this, i, &connections](){ + connections.erase(connections.begin() + i); + updateConnectionLines(true); + loadZoneConnectionMenuContent(); + changed(); + }); + widget->setCellWidget(i, 0, zoneA); + widget->setCellWidget(i, 1, zoneB); + widget->setCellWidget(i, 2, guardStrength); + widget->setCellWidget(i, 3, connectionType); + widget->setCellWidget(i, 4, hasRoad); + widget->setCellWidget(i, 5, delButton); + }; + widget->resizeColumnsToContents(); +} + +void TemplateEditor::saveZoneMenuContent() +{ + if(selectedZone < 0 || selectedTemplate.empty()) + return; + + auto zone = templates[selectedTemplate]->getZones().at(selectedZone); + auto type = static_cast(ui->comboBoxZoneType->currentData().toInt()); + + zone->setVisPosition(Point(ui->spinBoxZoneVisPosX->value(), ui->spinBoxZoneVisPosY->value())); + zone->setVisSize(ui->doubleSpinBoxZoneVisSize->value()); + zone->setType(type); + zone->setSize(ui->spinBoxZoneSize->value()); + zone->playerTowns.townCount = ui->spinBoxTownCountPlayer->value(); + zone->playerTowns.castleCount = ui->spinBoxCastleCountPlayer->value(); + zone->playerTowns.townDensity = ui->spinBoxTownDensityPlayer->value(); + zone->playerTowns.castleDensity = ui->spinBoxCastleDensityPlayer->value(); + zone->neutralTowns.townCount = ui->spinBoxTownCountNeutral->value(); + zone->neutralTowns.castleCount = ui->spinBoxCastleCountNeutral->value(); + zone->neutralTowns.townDensity = ui->spinBoxTownDensityNeutral->value(); + zone->neutralTowns.castleDensity = ui->spinBoxCastleDensityNeutral->value(); + zone->matchTerrainToTown = ui->checkBoxMatchTerrainToTown->isChecked(); + zone->townsAreSameType = ui->checkBoxTownsAreSameType->isChecked(); + zone->monsterStrength = static_cast(ui->comboBoxMonsterStrength->currentData().toInt()); + zone->id = ui->spinBoxZoneId->value(); + zone->townsLikeZone = ui->checkBoxZoneLinkTowns->isChecked() ? ui->spinBoxZoneLinkTowns->value() : rmg::ZoneOptions::NO_ZONE; + zone->minesLikeZone = ui->checkBoxZoneLinkMines->isChecked() ? ui->spinBoxZoneLinkMines->value() : rmg::ZoneOptions::NO_ZONE; + zone->terrainTypeLikeZone = ui->checkBoxZoneLinkTerrain->isChecked() ? ui->spinBoxZoneLinkTerrain->value() : rmg::ZoneOptions::NO_ZONE; + zone->treasureLikeZone = ui->checkBoxZoneLinkTreasure->isChecked() ? ui->spinBoxZoneLinkTreasure->value() : rmg::ZoneOptions::NO_ZONE; + zone->customObjectsLikeZone = ui->checkBoxZoneLinkCustomObjects->isChecked() ? ui->spinBoxZoneLinkCustomObjects->value() : rmg::ZoneOptions::NO_ZONE; + + if((type == ETemplateZoneType::PLAYER_START || type == ETemplateZoneType::CPU_START)) + zone->owner = std::optional(static_cast(ui->comboBoxZoneOwner->currentData().toInt())); + else + zone->owner = std::nullopt; + + updateZonePositions(); + updateZoneCards(selectedZone); + + changed(); +} + +void TemplateEditor::updateConnectionLines(bool recreate) +{ + if(recreate) + { + for(auto & line : lines) + templateScene->removeItem(line); + lines.clear(); + + for(int i = 0; i < templates[selectedTemplate]->getConnectedZoneIds().size(); i++) + { + auto & connection = templates[selectedTemplate]->getConnectedZoneIds()[i]; + auto line = new LineItem(); + line->setId(i); + line->setLineToolTip(tr("Zone A: %1\nZone B: %2\nGuard: %3").arg(QString::number(connection.zoneA)).arg(QString::number(connection.zoneB)).arg(QString::number(connection.guardStrength))); + line->setClickCallback([this, line](){ + ui->toolBox->setCurrentIndex(2); + ui->tableWidgetConnections->selectRow(line->getId()); + }); + lines.push_back(line); + templateScene->addItem(line); + } + } + + const auto& connections = templates[selectedTemplate]->getConnectedZoneIds(); + for (size_t i = 0; i < connections.size(); ++i) + { + auto& connection = connections[i]; + + if(!cards.count(connection.getZoneA()) || !cards.count(connection.getZoneB()) || i >= lines.size()) + continue; + + auto getCenter = [](auto & i){ + return i->pos() + QPointF(i->boundingRect().width() / 2, i->boundingRect().height() / 2) * i->scale(); + }; + auto posA = getCenter(cards[connection.getZoneA()]); + auto posB = getCenter(cards[connection.getZoneB()]); + + lines[i]->setLine(QLineF(posA.x(), posA.y(), posB.x(), posB.y())); + lines[i]->setText(QString::number(connection.getGuardStrength())); + + std::map pens = { + {rmg::EConnectionType::GUARDED, QPen(Qt::black, 5, Qt::SolidLine, Qt::SquareCap, Qt::BevelJoin)}, + {rmg::EConnectionType::FICTIVE, QPen(Qt::black, 5, Qt::DashLine, Qt::SquareCap, Qt::BevelJoin)}, + {rmg::EConnectionType::REPULSIVE, QPen(Qt::black, 5, Qt::DashDotLine, Qt::SquareCap, Qt::BevelJoin)}, + {rmg::EConnectionType::WIDE, QPen(Qt::black, 5, Qt::DotLine, Qt::SquareCap, Qt::BevelJoin)}, + {rmg::EConnectionType::FORCE_PORTAL, QPen(Qt::black, 5, Qt::DashDotDotLine, Qt::SquareCap, Qt::BevelJoin)} + }; + lines[i]->setPen(pens[connection.connectionType]); + } +} + +void TemplateEditor::saveContent() +{ + templates[selectedTemplate]->name = ui->lineEditName->text().toStdString(); + templates[selectedTemplate]->description = ui->lineEditDescription->text().toStdString(); + templates[selectedTemplate]->minSize = int3(ui->spinBoxMinSizeX->value(), ui->spinBoxMinSizeY->value(), ui->spinBoxMinSizeZ->value()); + templates[selectedTemplate]->maxSize = int3(ui->spinBoxMaxSizeX->value(), ui->spinBoxMaxSizeY->value(), ui->spinBoxMaxSizeZ->value()); + + templates[selectedTemplate]->allowedWaterContent.clear(); + if(ui->checkBoxAllowedWaterContentNone->isChecked()) + templates[selectedTemplate]->allowedWaterContent.insert(EWaterContent::EWaterContent::NONE); + if(ui->checkBoxAllowedWaterContentNormal->isChecked()) + templates[selectedTemplate]->allowedWaterContent.insert(EWaterContent::EWaterContent::NORMAL); + if(ui->checkBoxAllowedWaterContentIslands->isChecked()) + templates[selectedTemplate]->allowedWaterContent.insert(EWaterContent::EWaterContent::ISLANDS); + + changed(); +} + +bool TemplateEditor::getAnswerAboutUnsavedChanges() +{ + if(unsaved) + { + auto sure = QMessageBox::question(this, tr("Confirmation"), tr("Unsaved changes will be lost, are you sure?")); + if(sure == QMessageBox::No) + { + return false; + } + } + return true; +} + +void TemplateEditor::setTitle() +{ + QFileInfo fileInfo(filename); + QString title = QString("%1%2 - %3 (%4)").arg(fileInfo.fileName(), unsaved ? "*" : "", tr("VCMI Template Editor"), GameConstants::VCMI_VERSION.c_str()); + setWindowTitle(title); +} + +void TemplateEditor::changed() +{ + unsaved = true; + setTitle(); +} + +void TemplateEditor::saveTemplate() +{ + saveContent(); + + Helper::saveTemplate(templates, filename); + unsaved = false; +} + +void TemplateEditor::showTemplateEditor() +{ + auto * dialog = new TemplateEditor(); + + dialog->setAttribute(Qt::WA_DeleteOnClose); +} + +void TemplateEditor::on_actionOpen_triggered() +{ + if(!getAnswerAboutUnsavedChanges()) + return; + + auto filenameSelect = QFileDialog::getOpenFileName(this, tr("Open template"), + QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()), + tr("VCMI templates(*.json)")); + if(filenameSelect.isEmpty()) + return; + + templates = Helper::openTemplateInternal(filenameSelect); + + initContent(); + loadContent(); + ui->templateView->autoFit(); +} + +void TemplateEditor::on_actionSave_as_triggered() +{ + auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save template"), "", tr("VCMI templates (*.json)")); + + if(filenameSelect.isNull()) + return; + + QFileInfo fileInfo(filenameSelect); + + if(fileInfo.suffix().toLower() != "json") + filenameSelect += ".json"; + + filename = filenameSelect; + saveTemplate(); + setTitle(); +} + +void TemplateEditor::on_actionNew_triggered() +{ + if(!getAnswerAboutUnsavedChanges()) + return; + + templates = std::map>(); + templates["TemplateEditor"] = std::make_shared(); + + changed(); + initContent(); + loadContent(); + ui->templateView->autoFit(); +} + +void TemplateEditor::on_actionSave_triggered() +{ + if(filename.isNull()) + on_actionSave_as_triggered(); + else + saveTemplate(); + setTitle(); +} + +void TemplateEditor::on_actionAutoPosition_triggered() +{ + if(!templates.size()) + return; + + saveContent(); + loadContent(true); + ui->templateView->autoFit(); +} + +void TemplateEditor::on_actionZoom_in_triggered() +{ + ui->templateView->changeZoomLevel(true); +} + +void TemplateEditor::on_actionZoom_out_triggered() +{ + ui->templateView->changeZoomLevel(false); +} + +void TemplateEditor::on_actionZoom_auto_triggered() +{ + ui->templateView->autoFit(); +} + +void TemplateEditor::on_actionZoom_reset_triggered() +{ + ui->templateView->setZoomLevel(0); +} + +void TemplateEditor::on_actionAddZone_triggered() +{ + if(!templates.size()) + return; + + for(int i = 1; i < 999; i++) + { + if(!templates[selectedTemplate]->zones.count(i)) + { + templates[selectedTemplate]->zones[i] = std::make_shared(); + break; + } + } + loadContent(); +} + +void TemplateEditor::on_actionRemoveZone_triggered() +{ + templates[selectedTemplate]->zones.erase(selectedZone); + selectedZone = -1; + loadContent(); +} + +void TemplateEditor::on_comboBoxTemplateSelection_activated(int index) +{ + auto value = ui->comboBoxTemplateSelection->currentText().toStdString(); + if(value.empty()) + return; + + saveContent(); + selectedTemplate = value; + loadContent(); +} + +void TemplateEditor::closeEvent(QCloseEvent *event) +{ + if(getAnswerAboutUnsavedChanges()) + QWidget::closeEvent(event); + else + event->ignore(); +} + +void TemplateEditor::on_pushButtonAddSubTemplate_clicked() +{ + bool ok; + QString text = QInputDialog::getText(this, tr("Enter Name"), tr("Name:"), QLineEdit::Normal, "", &ok); + + if (!ok || text.isEmpty()) + return; + + if(templates.count(text.toStdString())) + { + QMessageBox::critical(this, tr("Already existing!"), tr("At template with this name is already existing.")); + return; + } + + selectedTemplate = text.toStdString(); + templates[selectedTemplate] = std::make_shared(); + ui->comboBoxTemplateSelection->addItem(text); + ui->comboBoxTemplateSelection->setCurrentIndex(ui->comboBoxTemplateSelection->count() - 1); + + loadContent(); +} + +void TemplateEditor::on_pushButtonRemoveSubTemplate_clicked() +{ + if(templates.size() < 2) + { + QMessageBox::critical(this, tr("To few templates!"), tr("At least one template should remain after removing.")); + return; + } + + templates.erase(selectedTemplate); + ui->comboBoxTemplateSelection->removeItem(ui->comboBoxTemplateSelection->currentIndex()); + selectedTemplate = ui->comboBoxTemplateSelection->currentText().toStdString(); + + loadContent(); +} + +void TemplateEditor::on_pushButtonRenameSubTemplate_clicked() +{ + if(ui->comboBoxTemplateSelection->currentText().isEmpty() || !templates.size()) + return; + + bool ok; + QString text = QInputDialog::getText(this, tr("Enter Name"), tr("Name:"), QLineEdit::Normal, ui->comboBoxTemplateSelection->currentText(), &ok); + + if (!ok || text.isEmpty()) + return; + + ui->comboBoxTemplateSelection->setItemText(ui->comboBoxTemplateSelection->currentIndex(), text); + + templates[text.toStdString()] = templates[selectedTemplate]; + templates.erase(selectedTemplate); + selectedTemplate = text.toStdString(); +} + +void TemplateEditor::on_spinBoxZoneVisPosX_valueChanged() +{ + if(ui->spinBoxZoneVisPosX->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneVisPosY_valueChanged() +{ + if(ui->spinBoxZoneVisPosY->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_doubleSpinBoxZoneVisSize_valueChanged() +{ + if(ui->doubleSpinBoxZoneVisSize->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_comboBoxZoneType_currentTextChanged(const QString &text) +{ + if(!ui->comboBoxZoneType->hasFocus()) + return; + ui->comboBoxZoneType->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_comboBoxZoneOwner_currentTextChanged(const QString &text) +{ + if(ui->comboBoxZoneOwner->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneSize_valueChanged() +{ + if(ui->spinBoxZoneSize->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxTownCountPlayer_valueChanged() +{ + if(ui->spinBoxTownCountPlayer->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxCastleCountPlayer_valueChanged() +{ + if(ui->spinBoxCastleCountPlayer->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxTownDensityPlayer_valueChanged() +{ + if(ui->spinBoxTownDensityPlayer->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxCastleDensityPlayer_valueChanged() +{ + if(ui->spinBoxCastleDensityPlayer->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxTownCountNeutral_valueChanged() +{ + if(ui->spinBoxTownCountNeutral->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxCastleCountNeutral_valueChanged() +{ + if(ui->spinBoxCastleCountNeutral->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxTownDensityNeutral_valueChanged() +{ + if(ui->spinBoxTownDensityNeutral->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxCastleDensityNeutral_valueChanged() +{ + if(ui->spinBoxCastleDensityNeutral->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxMatchTerrainToTown_stateChanged(int state) +{ + if(ui->checkBoxMatchTerrainToTown->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxTownsAreSameType_stateChanged(int state) +{ + if(ui->checkBoxTownsAreSameType->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_comboBoxMonsterStrength_currentTextChanged(const QString &text) +{ + if(ui->comboBoxMonsterStrength->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneId_valueChanged() +{ + if(ui->spinBoxZoneId->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneLinkTowns_valueChanged() +{ + if(ui->spinBoxZoneLinkTowns->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneLinkMines_valueChanged() +{ + if(ui->spinBoxZoneLinkMines->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneLinkTerrain_valueChanged() +{ + if(ui->spinBoxZoneLinkTerrain->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneLinkTreasure_valueChanged() +{ + if(ui->spinBoxZoneLinkTreasure->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_spinBoxZoneLinkCustomObjects_valueChanged() +{ + if(ui->spinBoxZoneLinkCustomObjects->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxZoneLinkTowns_stateChanged(int state) +{ + if(!ui->checkBoxZoneLinkTowns->hasFocus()) + return; + ui->checkBoxZoneLinkTowns->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxZoneLinkMines_stateChanged(int state) +{ + if(!ui->checkBoxZoneLinkMines->hasFocus()) + return; + ui->checkBoxZoneLinkMines->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxZoneLinkTerrain_stateChanged(int state) +{ + if(!ui->checkBoxZoneLinkTerrain->hasFocus()) + return; + ui->checkBoxZoneLinkTerrain->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxZoneLinkTreasure_stateChanged(int state) +{ + if(!ui->checkBoxZoneLinkTreasure->hasFocus()) + return; + ui->checkBoxZoneLinkTreasure->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxZoneLinkCustomObjects_stateChanged(int state) +{ + if(!ui->checkBoxZoneLinkCustomObjects->hasFocus()) + return; + ui->checkBoxZoneLinkCustomObjects->clearFocus(); + + saveZoneMenuContent(); + loadZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxAllowedWaterContentNone_stateChanged(int state) +{ + if(ui->checkBoxAllowedWaterContentNone->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxAllowedWaterContentNormal_stateChanged(int state) +{ + if(ui->checkBoxAllowedWaterContentNormal->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_checkBoxAllowedWaterContentIslands_stateChanged(int state) +{ + if(ui->checkBoxAllowedWaterContentIslands->hasFocus()) + saveZoneMenuContent(); +} + +void TemplateEditor::on_pushButtonConnectionAdd_clicked() +{ + auto & connections = templates[selectedTemplate]->connections; + connections.push_back(rmg::ZoneConnection()); + loadZoneConnectionMenuContent(); +} + +void TemplateEditor::on_pushButtonOpenTerrainTypes_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonOpenBannedTerrainTypes_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonAllowedTowns_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonBannedTowns_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonTownHints_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonAllowedMonsters_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonBannedMonsters_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonTreasure_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonMines_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} + +void TemplateEditor::on_pushButtonCustomObjects_clicked() +{ + //TODO: Implement dialog + QMessageBox::critical(this, tr("Error"), tr("Not implemented yet!")); +} diff --git a/mapeditor/templateeditor/templateeditor.h b/mapeditor/templateeditor/templateeditor.h new file mode 100644 index 000000000..bcf417076 --- /dev/null +++ b/mapeditor/templateeditor/templateeditor.h @@ -0,0 +1,131 @@ +/* + * templateeditor.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once +#include + +#include "templateview.h" + +#include "../StdInc.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/GameConstants.h" + +class CRmgTemplate; +class CardItem; +class LineItem; +class ZoneOptions; +namespace rmg { +class ZoneOptions; +} + +namespace Ui { +class TemplateEditor; +} + +class TemplateEditor : public QWidget +{ + Q_OBJECT + +public: + explicit TemplateEditor(); + ~TemplateEditor(); + + static void showTemplateEditor(); + +private slots: + void on_actionOpen_triggered(); + void on_actionSave_as_triggered(); + void on_actionNew_triggered(); + void on_actionSave_triggered(); + void on_actionAutoPosition_triggered(); + void on_actionZoom_in_triggered(); + void on_actionZoom_out_triggered(); + void on_actionZoom_auto_triggered(); + void on_actionZoom_reset_triggered(); + void on_actionAddZone_triggered(); + void on_actionRemoveZone_triggered(); + void on_comboBoxTemplateSelection_activated(int index); + void on_pushButtonAddSubTemplate_clicked(); + void on_pushButtonRemoveSubTemplate_clicked(); + void on_pushButtonRenameSubTemplate_clicked(); + void on_spinBoxZoneVisPosX_valueChanged(); + void on_spinBoxZoneVisPosY_valueChanged(); + void on_doubleSpinBoxZoneVisSize_valueChanged(); + void on_comboBoxZoneType_currentTextChanged(const QString &text); + void on_comboBoxZoneOwner_currentTextChanged(const QString &text); + void on_spinBoxZoneSize_valueChanged(); + void on_spinBoxTownCountPlayer_valueChanged(); + void on_spinBoxCastleCountPlayer_valueChanged(); + void on_spinBoxTownDensityPlayer_valueChanged(); + void on_spinBoxCastleDensityPlayer_valueChanged(); + void on_spinBoxTownCountNeutral_valueChanged(); + void on_spinBoxCastleCountNeutral_valueChanged(); + void on_spinBoxTownDensityNeutral_valueChanged(); + void on_spinBoxCastleDensityNeutral_valueChanged(); + void on_checkBoxMatchTerrainToTown_stateChanged(int state); + void on_checkBoxTownsAreSameType_stateChanged(int state); + void on_comboBoxMonsterStrength_currentTextChanged(const QString &text); + void on_spinBoxZoneId_valueChanged(); + void on_spinBoxZoneLinkTowns_valueChanged(); + void on_spinBoxZoneLinkMines_valueChanged(); + void on_spinBoxZoneLinkTerrain_valueChanged(); + void on_spinBoxZoneLinkTreasure_valueChanged(); + void on_spinBoxZoneLinkCustomObjects_valueChanged(); + void on_checkBoxZoneLinkTowns_stateChanged(int state); + void on_checkBoxZoneLinkMines_stateChanged(int state); + void on_checkBoxZoneLinkTerrain_stateChanged(int state); + void on_checkBoxZoneLinkTreasure_stateChanged(int state); + void on_checkBoxZoneLinkCustomObjects_stateChanged(int state); + void on_checkBoxAllowedWaterContentNone_stateChanged(int state); + void on_checkBoxAllowedWaterContentNormal_stateChanged(int state); + void on_checkBoxAllowedWaterContentIslands_stateChanged(int state); + void on_pushButtonConnectionAdd_clicked(); + void on_pushButtonOpenTerrainTypes_clicked(); + void on_pushButtonOpenBannedTerrainTypes_clicked(); + void on_pushButtonAllowedTowns_clicked(); + void on_pushButtonBannedTowns_clicked(); + void on_pushButtonTownHints_clicked(); + void on_pushButtonAllowedMonsters_clicked(); + void on_pushButtonBannedMonsters_clicked(); + void on_pushButtonTreasure_clicked(); + void on_pushButtonMines_clicked(); + void on_pushButtonCustomObjects_clicked(); + +private: + bool getAnswerAboutUnsavedChanges(); + void setTitle(); + void changed(); + void saveTemplate(); + void initContent(); + void loadContent(bool autoPosition = false); + void saveContent(); + void loadZoneMenuContent(bool onlyPosition = false); + void saveZoneMenuContent(); + void loadZoneConnectionMenuContent(); + void updateConnectionLines(bool recreate = false); + void autoPositionZones(); + void updateZonePositions(); + QString getZoneToolTip(std::shared_ptr zone); + void updateZoneCards(TRmgTemplateZoneId id = -1); + + void closeEvent(QCloseEvent *event) override; + + Ui::TemplateEditor *ui; + + std::unique_ptr templateScene; + + QString filename; + bool unsaved = false; + std::map> templates; + std::string selectedTemplate; + int selectedZone; + + std::map cards; + std::vector lines; +}; diff --git a/mapeditor/templateeditor/templateeditor.ui b/mapeditor/templateeditor/templateeditor.ui new file mode 100644 index 000000000..721fe58ea --- /dev/null +++ b/mapeditor/templateeditor/templateeditor.ui @@ -0,0 +1,1393 @@ + + + TemplateEditor + + + + 0 + 0 + 1024 + 720 + + + + VCMI Template Editor + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + File + + + + + + + + + Edit + + + + + + + + View + + + + + + + + + + + + + + + Toolbar + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 10 + 20 + + + + + + + + Selected Template: + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + + + + Add + + + + + + + Remove + + + + + + + Rename + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 12 + 0 + + + + true + + + QAbstractScrollArea::AdjustToContents + + + + + + + + 10 + 0 + + + + + 600 + 16777215 + + + + 1 + + + + + 0 + -148 + 449 + 662 + + + + General + + + + + + Name + + + + + + + + + + + + Description + + + + + + + + + + + + Min Size + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + X + + + + + + + 1 + + + 999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Y + + + + + + + 1 + + + 999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Z + + + + + + + 1 + + + 2 + + + + + + + + + + Max Size + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + X + + + + + + + 1 + + + 999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Y + + + + + + + 1 + + + 999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Z + + + + + + + 1 + + + 2 + + + + + + + + + + Players + + + + + + Players + + + + + + + + + + Human + + + + + + + + + + + + + Allowed water content + + + + + + None + + + + + + + Normal + + + + + + + Islands + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + -980 + 449 + 1494 + + + + Zone + + + + + + Visualisation + + + + + + Position + + + + + + + 0 + + + + + X + + + + + + + -9999 + + + 9999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Y + + + + + + + -9999 + + + 9999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Size + + + + + + + 1 + + + 0.100000000000000 + + + 9.900000000000000 + + + 0.100000000000000 + + + + + + + + + + + + ID + + + + + + 999 + + + + + + + + + + Type + + + + + + + + + + + + Size + + + + + + 99999 + + + + + + + + + + Owner + + + + + + + + + + + + Zone link + + + + + + Mines + + + + + + + Custom objects + + + + + + + Towns + + + + + + + Terrain + + + + + + + 0 + + + 999 + + + + + + + Treasure + + + + + + + 0 + + + 999 + + + + + + + 0 + + + 999 + + + + + + + 0 + + + 999 + + + + + + + 0 + + + 999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Town info + + + + + + Town count + + + + + + + 0 + + + + + Player + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Neutral + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Castle count + + + + + + + 0 + + + + + Player + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Neutral + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Town density + + + + + + + 0 + + + + + Player + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Neutral + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Castle density + + + + + + + 0 + + + + + Player + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Neutral + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Terrain + + + + + + Match terrain to town + + + + + + + Terrain types + + + + + + + Banned terrain types + + + + + + + + + + Towns + + + + + + Towns are same type + + + + + + + Allowed towns + + + + + + + Banned towns + + + + + + + Town hints + + + + + + + + + + Monsters + + + + + + Allowed monsters + + + + + + + Banned monsters + + + + + + + 0 + + + + + + 0 + 0 + + + + Strength + + + + + + + + 0 + 0 + + + + + + + + + + + + + Treasure + + + + + + Treasure + + + + + + + + + + Mines + + + + + + Mines + + + + + + + + + + Objects + + + + + + Custom objects + + + + + + + + + + Qt::Vertical + + + + 20 + 526 + + + + + + + + + + 0 + 0 + 462 + 514 + + + + Connections + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 1 + 0 + + + + Add + + + + + + + + + + + + + + + + + + Open + + + Ctrl+O + + + + + Save + + + Ctrl+S + + + + + New + + + Ctrl+N + + + + + Save as... + + + Ctrl+Shift+S + + + + + Add zone + + + Z + + + + + false + + + Remove zone + + + Del + + + + + Auto position + + + Ctrl+P + + + + + Zoom in + + + Ctrl++ + + + + + Zoom out + + + Ctrl+- + + + + + Zoom auto + + + Ctrl+Shift+: + + + + + Zoom reset + + + Ctrl+Shift+= + + + + + + TemplateView + QGraphicsView +
templateeditor/templateview.h
+
+
+ + +
diff --git a/mapeditor/templateeditor/templateview.cpp b/mapeditor/templateeditor/templateview.cpp new file mode 100644 index 000000000..ca8b8f99e --- /dev/null +++ b/mapeditor/templateeditor/templateview.cpp @@ -0,0 +1,60 @@ +/* + * templateview.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "templateview.h" + +TemplateScene::TemplateScene(): + QGraphicsScene(nullptr) +{ +} + +TemplateView::TemplateView(QWidget * parent): + QGraphicsView(parent) +{ +} + +void TemplateView::setZoomLevel(int level) +{ + zoomlevel = level; + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + float scale = pow(1.1, zoomlevel); + QTransform matrix; + matrix.scale(scale, scale); + setTransform(matrix); +} + +void TemplateView::changeZoomLevel(bool increase) +{ + if(increase) + zoomlevel++; + else + zoomlevel--; + + setZoomLevel(zoomlevel); +} + +void TemplateView::autoFit() +{ + fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); + + zoomlevel = log(transform().m11()) / log(1.1f); +} + +void TemplateView::wheelEvent(QWheelEvent *e) +{ + if (e->angleDelta().y() > 0) + changeZoomLevel(true); + else + changeZoomLevel(false); + + e->accept(); +} diff --git a/mapeditor/templateeditor/templateview.h b/mapeditor/templateeditor/templateview.h new file mode 100644 index 000000000..619b14c0c --- /dev/null +++ b/mapeditor/templateeditor/templateview.h @@ -0,0 +1,37 @@ +/* + * templateview.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include +#include + +class TemplateScene : public QGraphicsScene +{ + Q_OBJECT; +public: + TemplateScene(); +}; + +class TemplateView : public QGraphicsView +{ + Q_OBJECT + + int zoomlevel = 0; + +public: + TemplateView(QWidget * parent); + + void setZoomLevel(int level); + void changeZoomLevel(bool increase); + void autoFit(); + + void wheelEvent(QWheelEvent * e) override; +};