Browse Source

PlayerSelectionDialog implementation + code cleanup

MichalZr6 7 months ago
parent
commit
9964545e7a

+ 2 - 0
mapeditor/CMakeLists.txt

@@ -20,6 +20,7 @@ set(editor_SRCS
 		mapsettings/eventsettings.cpp
 		mapsettings/eventsettings.cpp
 		mapsettings/rumorsettings.cpp
 		mapsettings/rumorsettings.cpp
 		mapsettings/translations.cpp
 		mapsettings/translations.cpp
+		PlayerSelectionDialog.cpp
 		playersettings.cpp
 		playersettings.cpp
 		playerparams.cpp
 		playerparams.cpp
 		scenelayer.cpp
 		scenelayer.cpp
@@ -70,6 +71,7 @@ set(editor_HEADERS
 		mapsettings/eventsettings.h
 		mapsettings/eventsettings.h
 		mapsettings/rumorsettings.h
 		mapsettings/rumorsettings.h
 		mapsettings/translations.h
 		mapsettings/translations.h
+		PlayerSelectionDialog.h
 		playersettings.h
 		playersettings.h
 		playerparams.h
 		playerparams.h
 		scenelayer.h
 		scenelayer.h

+ 156 - 0
mapeditor/PlayerSelectionDialog.cpp

@@ -0,0 +1,156 @@
+/*
+ * PlayerSelectionDialog.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 "PlayerSelectionDialog.h"
+#include "mainwindow.h"
+#include "../lib/mapping/CMap.h"
+
+PlayerSelectionDialog::PlayerSelectionDialog(QWidget * parent)
+	: QDialog(parent), selectedPlayer(PlayerColor::NEUTRAL)
+{
+	auto main = qobject_cast<MainWindow *>(parent);
+	assert(main);
+
+	setupDialogComponents();
+
+	int maxPlayers = 0;
+	if(main && main->controller.map())
+		maxPlayers = main->controller.map()->players.size();
+	
+	bool defaultIsChecked = false;
+
+	for(int i = 0; i < maxPlayers; ++i)
+	{
+		PlayerColor player(i);
+		QAction * action = main->getActionPlayer(player);
+
+		addCheckbox(action, player, main->controller.map()->players.at(i).canAnyonePlay(), & defaultIsChecked);
+	}
+}
+
+void PlayerSelectionDialog::onCheckboxToggled(bool checked)
+{
+	if(!checked)
+		return;
+
+	QCheckBox * senderCheckBox = qobject_cast<QCheckBox *>(sender());
+	if(!senderCheckBox)
+		return;
+
+	for(int i = 0; i < checkboxes.size(); ++i)
+	{
+		QCheckBox * cb = checkboxes[i];
+		if(cb == senderCheckBox)
+		{
+			selectedPlayer = PlayerColor(i);
+		}
+		else
+		{
+			cb->blockSignals(true);
+			cb->setChecked(false);
+			cb->blockSignals(false);
+		}
+	}
+}
+
+
+PlayerColor PlayerSelectionDialog::getSelectedPlayer() const
+{
+	return selectedPlayer;
+}
+
+void PlayerSelectionDialog::setupDialogComponents()
+{
+	setWindowTitle(tr("Select Player"));
+	setFixedSize(sizeHint());
+	setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+	font.setPointSize(10);
+	setFont(font);
+
+	mainLayout.setSpacing(10);
+	mainLayout.setContentsMargins(20, 20, 80, 20);
+	checkboxLayout.setContentsMargins(0, 10, 0, 20);
+	mainLayout.addSpacing(4);
+
+	QLabel * errorLabel = new QLabel(tr("Hero cannot be created as NEUTRAL"), this);
+	font.setBold(true);
+	errorLabel->setFont(font);
+	mainLayout.addWidget(errorLabel);
+
+	QLabel * instructionLabel = new QLabel(tr("Switch to one of the available players:"), this);
+	font.setBold(false);
+	instructionLabel->setFont(font);
+	mainLayout.addWidget(instructionLabel);
+
+	QWidget * checkboxContainer = new QWidget(this);
+	checkboxContainer->setLayout(& checkboxLayout);
+	mainLayout.addWidget(checkboxContainer);
+
+	QDialogButtonBox * box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+	connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+	connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+	mainLayout.addWidget(box);
+
+	setLayout(& mainLayout);
+}
+
+void PlayerSelectionDialog::addCheckbox(QAction * checkboxAction, PlayerColor player, bool isEnabled, bool * isDefault)
+{
+	QHBoxLayout * rowLayout = new QHBoxLayout();
+	auto * checkbox = new QCheckBox(checkboxAction->text(), this);
+	
+	QLabel * shortcutLabel = new QLabel(checkboxAction->shortcut().toString(), this);
+	QFont shortcutFont = font;
+	shortcutFont.setPointSize(9);
+	shortcutFont.setItalic(true);
+	shortcutLabel->setFont(shortcutFont);
+	shortcutLabel->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
+
+	QWidget * checkboxContainer = new QWidget(this);
+	QHBoxLayout * cbLayout = new QHBoxLayout(checkboxContainer);
+	cbLayout->setContentsMargins(0, 0, 0, 0);
+	cbLayout->addWidget(checkbox, 0, Qt::AlignCenter);
+
+	rowLayout->addWidget(checkboxContainer, 1);
+	rowLayout->addWidget(shortcutLabel, 1);
+
+	checkbox->setEnabled(isEnabled);
+	if(checkbox->isEnabled() && !*isDefault)
+	{
+		checkbox->setChecked(true);
+		selectedPlayer = player;
+		*isDefault = true;
+	}
+
+	checkboxLayout.addLayout(rowLayout);
+
+	connect(checkbox, &QCheckBox::clicked, this, [=]()
+		{
+			selectedPlayer = player;
+
+			// Radio-style logic: uncheck other boxes
+			for(auto * box : findChildren<QCheckBox *>())
+			{
+				if(box != checkbox)
+					box->setChecked(false);
+			}
+		});
+
+
+	// Add action to the dialog for shortcut support
+	addAction(checkboxAction);
+
+	// Connect action trigger to simulate checkbox click
+	connect(checkboxAction, &QAction::triggered, this, [=]()
+		{
+			if(checkbox->isEnabled())
+				checkbox->click();
+		});
+}

+ 44 - 0
mapeditor/PlayerSelectionDialog.h

@@ -0,0 +1,44 @@
+/*
+ * PlayerSelectionDialog.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 <QDialog>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QDialogButtonBox>
+#include "../source/lib/constants/EntityIdentifiers.h"
+
+/// Dialog shown when a hero cannot be placed as NEUTRAL.
+/// Allows the user to select a valid player via checkboxes,
+/// or using the existing keyboard shortcuts from MainWindow's player QActions.
+class PlayerSelectionDialog : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit PlayerSelectionDialog(QWidget * parent = nullptr);
+	PlayerColor getSelectedPlayer() const;
+
+private slots:
+	void onCheckboxToggled(bool checked);
+
+private:
+	std::vector<QCheckBox *> checkboxes;
+	PlayerColor selectedPlayer;
+
+	QFont font;
+	QVBoxLayout mainLayout;
+	QVBoxLayout checkboxLayout;
+
+	void setupDialogComponents();
+	void addCheckbox(QAction * checkboxAction, PlayerColor player, bool isEnabled, bool * isDefault);
+
+};

+ 9 - 8
mapeditor/mainwindow.h

@@ -25,7 +25,7 @@ namespace Ui
 
 
 class MainWindow : public QMainWindow
 class MainWindow : public QMainWindow
 {
 {
-    Q_OBJECT
+	Q_OBJECT
 
 
 	const QString mainWindowSizeSetting = "MainWindow/Size";
 	const QString mainWindowSizeSetting = "MainWindow/Size";
 	const QString mainWindowPositionSetting = "MainWindow/Position";
 	const QString mainWindowPositionSetting = "MainWindow/Position";
@@ -42,8 +42,8 @@ class MainWindow : public QMainWindow
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 
 
 public:
 public:
-    explicit MainWindow(QWidget *parent = nullptr);
-    ~MainWindow();
+	explicit MainWindow(QWidget *parent = nullptr);
+	~MainWindow();
 
 
 	void initializeMap(bool isNew);
 	void initializeMap(bool isNew);
 
 
@@ -62,6 +62,11 @@ public:
 
 
 	void loadTranslation();
 	void loadTranslation();
 
 
+	QAction * getActionPlayer(const PlayerColor &);
+
+public slots:
+	void switchDefaultPlayer(const PlayerColor &);
+
 private slots:
 private slots:
 	void on_actionOpen_triggered();
 	void on_actionOpen_triggered();
 	
 	
@@ -110,8 +115,6 @@ private slots:
 	void on_actionUpdate_appearance_triggered();
 	void on_actionUpdate_appearance_triggered();
 
 
 	void on_actionRecreate_obstacles_triggered();
 	void on_actionRecreate_obstacles_triggered();
-	
-	void switchDefaultPlayer(const PlayerColor &);
 
 
 	void on_actionCut_triggered();
 	void on_actionCut_triggered();
 
 
@@ -169,8 +172,6 @@ private:
 	void preparePreview(const QModelIndex & index);
 	void preparePreview(const QModelIndex & index);
 	void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
 	void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
 	void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
 	void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
-	
-	QAction * getActionPlayer(const PlayerColor &);
 
 
 	void changeBrushState(int idx);
 	void changeBrushState(int idx);
 	void setTitle();
 	void setTitle();
@@ -187,7 +188,7 @@ private:
 	void updateRecentMenu(const QString & filenameSelect);
 	void updateRecentMenu(const QString & filenameSelect);
 
 
 private:
 private:
-    Ui::MainWindow * ui;
+	Ui::MainWindow * ui;
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	QGraphicsScene * scenePreview;
 	QGraphicsScene * scenePreview;
 	MapSettings * mapSettings = nullptr;
 	MapSettings * mapSettings = nullptr;

+ 59 - 36
mapeditor/mapcontroller.cpp

@@ -36,12 +36,13 @@
 #include "mainwindow.h"
 #include "mainwindow.h"
 #include "inspector/inspector.h"
 #include "inspector/inspector.h"
 #include "GameLibrary.h"
 #include "GameLibrary.h"
+#include "PlayerSelectionDialog.h"
+
+MapController::MapController(QObject * parent)
+	: QObject(parent)
+{
+}
 
 
-MapController::MapController(QObject * parent)
-	: QObject(parent)
-{
-}
-
 MapController::MapController(MainWindow * m): main(m)
 MapController::MapController(MainWindow * m): main(m)
 {
 {
 	for(int i : {0, 1})
 	for(int i : {0, 1})
@@ -371,7 +372,7 @@ void MapController::pasteFromClipboard(int level)
 	{
 	{
 		auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
 		auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
 		QString errorMsg;
 		QString errorMsg;
-		if (!canPlaceObject(level, obj.get(), errorMsg))
+		if(!canPlaceObject(obj.get(), errorMsg))
 		{
 		{
 			errors.push_back(std::move(errorMsg));
 			errors.push_back(std::move(errorMsg));
 			continue;
 			continue;
@@ -542,36 +543,59 @@ void MapController::commitObjectCreate(int level)
 	main->mapChanged();
 	main->mapChanged();
 }
 }
 
 
-bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString & error) const
+bool MapController::canPlaceObject(const CGObjectInstance * newObj, QString & error) const
 {	
 {	
 	if(newObj->ID == Obj::GRAIL) //special case for grail
 	if(newObj->ID == Obj::GRAIL) //special case for grail
+		return canPlaceGrail(newObj, error);
+	
+	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+		return canPlaceHero(newObj, error);
+	
+	return checkRequiredMods(newObj, error);
+}
+
+bool MapController::canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const
+{
+	assert(grailObj->ID == Obj::GRAIL);
+
+	//find all objects of such type
+	int objCounter = 0;
+	for(auto o : _map->objects)
 	{
 	{
-		//find all objects of such type
-		int objCounter = 0;
-		for(auto o : _map->objects)
+		if(o->ID == grailObj->ID && o->subID == grailObj->subID)
 		{
 		{
-			if(o->ID == newObj->ID && o->subID == newObj->subID)
-			{
-				++objCounter;
-			}
+			++objCounter;
 		}
 		}
+	}
 
 
-		if(objCounter >= 1)
-		{
-			error = QObject::tr("There can only be one grail object on the map.");
-			return false; //maplimit reached
-		}
+	if(objCounter >= 1)
+	{
+		error = QObject::tr("There can only be one grail object on the map.");
+		return false; //maplimit reached
 	}
 	}
 	
 	
-	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+	return true;
+}
+
+bool MapController::canPlaceHero(const CGObjectInstance * heroObj, QString & error) const
+{
+	assert(heroObj->ID == Obj::HERO || heroObj->ID == Obj::RANDOM_HERO);
+
+	PlayerSelectionDialog dialog(main);
+	if(dialog.exec() == QDialog::Accepted)
 	{
 	{
-		error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName));
-		return false;
+		main->switchDefaultPlayer(dialog.getSelectedPlayer());
+		return true;
 	}
 	}
+	
+	error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(heroObj->instanceName));
+	return false;
+}
 
 
-	// check if object's mod is in required mods in map settings
+bool MapController::checkRequiredMods(const CGObjectInstance * obj, QString & error) const
+{
 	ModCompatibilityInfo modsInfo;
 	ModCompatibilityInfo modsInfo;
-	modAssessmentObject(newObj, modsInfo);
+	modAssessmentObject(obj, modsInfo);
 
 
 	for(auto & mod : modsInfo)
 	for(auto & mod : modsInfo)
 	{
 	{
@@ -581,24 +605,23 @@ bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString
 			if(!mod.second.parent.empty())
 			if(!mod.second.parent.empty())
 				submod = " (" + tr("submod of") + " " + QString::fromStdString(mod.second.parent) + ")";
 				submod = " (" + tr("submod of") + " " + QString::fromStdString(mod.second.parent) + ")";
 
 
-			auto reply = QMessageBox::question(main,
-				tr("Required Mod Missing"),
-				tr("This object is from the mod '%1'%2.\n"
-					"The mod is currently not in the map's required modifications list.\n"
-					"Do you want to add this mod to the required modifications ?")
-				.arg(QString::fromStdString(LIBRARY->modh->getModInfo(mod.first).getVerificationInfo().name), submod),
+			auto reply = QMessageBox::question(main,
+				tr("Missing Required Mod"),
+				tr("This object is from the mod '%1'%2.\n\n"
+					"The mod is currently not in the map's required modifications list.\n\n"
+					"Do you want to add this mod to the required modifications ?\n")
+				.arg(QString::fromStdString(LIBRARY->modh->getModInfo(mod.first).getVerificationInfo().name), submod),
 				QMessageBox::Yes | QMessageBox::No);
 				QMessageBox::Yes | QMessageBox::No);
 
 
 			if(reply == QMessageBox::Yes)
 			if(reply == QMessageBox::Yes)
-				requestModsUpdate(modsInfo, true);		// emit signal for MapSettings
+				/* emit */ requestModsUpdate(modsInfo, true); // signal for MapSettings
 			else
 			else
 			{
 			{
-				error = tr("The object's mod is mandatory for map to remain valid.");
+				error = tr("This object's mod is mandatory for map to remain valid.");
 				return false;
 				return false;
 			}
 			}
 		}
 		}
 	}
 	}
-	
 	return true;
 	return true;
 }
 }
 
 
@@ -634,7 +657,7 @@ ModCompatibilityInfo MapController::modAssessmentAll()
 	return result;
 	return result;
 }
 }
 
 
-void MapController::modAssessmentObject(CGObjectInstance * obj, ModCompatibilityInfo & result)
+void MapController::modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result)
 {
 {
 	auto extractEntityMod = [&result](const auto & entity)
 	auto extractEntityMod = [&result](const auto & entity)
 	{
 	{
@@ -650,7 +673,7 @@ void MapController::modAssessmentObject(CGObjectInstance * obj, ModCompatibility
 
 
 	if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
 	if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
 	{
 	{
-		auto town = dynamic_cast<CGTownInstance *>(obj);
+		auto town = dynamic_cast<const CGTownInstance *>(obj);
 		for(const auto & spellID : town->possibleSpells)
 		for(const auto & spellID : town->possibleSpells)
 		{
 		{
 			if(spellID == SpellID::PRESET)
 			if(spellID == SpellID::PRESET)
@@ -682,7 +705,7 @@ void MapController::modAssessmentObject(CGObjectInstance * obj, ModCompatibility
 
 
 	if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
 	if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
 	{
 	{
-		auto hero = dynamic_cast<CGHeroInstance *>(obj);
+		auto hero = dynamic_cast<const CGHeroInstance *>(obj);
 		for(const auto & spellID : hero->getSpellsInSpellbook())
 		for(const auto & spellID : hero->getSpellsInSpellbook())
 		{
 		{
 			if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)
 			if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)

+ 9 - 2
mapeditor/mapcontroller.h

@@ -63,9 +63,16 @@ public:
 	
 	
 	bool discardObject(int level) const;
 	bool discardObject(int level) const;
 	void createObject(int level, std::shared_ptr<CGObjectInstance> obj) const;
 	void createObject(int level, std::shared_ptr<CGObjectInstance> obj) const;
-	bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const;
+	bool canPlaceObject(const CGObjectInstance * obj, QString & error) const;
+	bool canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const;
+	bool canPlaceHero(const CGObjectInstance * heroObj, QString & error) const;
 	
 	
-	static void modAssessmentObject(CGObjectInstance * obj, ModCompatibilityInfo & result);
+	/// Ensures that the object's mod is listed in the map's required mods.
+	/// If the mod is missing, prompts the user to add it. Returns false if the user declines,
+	/// making the object invalid for placement.
+	bool checkRequiredMods(const CGObjectInstance * obj, QString & error) const;
+	
+	static void modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result);
 	static ModCompatibilityInfo modAssessmentAll();
 	static ModCompatibilityInfo modAssessmentAll();
 	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 
 

+ 1 - 1
mapeditor/mapview.cpp

@@ -614,7 +614,7 @@ void MapView::dropEvent(QDropEvent * event)
 	if(sc->selectionObjectsView.newObject)
 	if(sc->selectionObjectsView.newObject)
 	{
 	{
 		QString errorMsg;
 		QString errorMsg;
-		if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject.get(), errorMsg))
+		if(controller->canPlaceObject(sc->selectionObjectsView.newObject, errorMsg))
 		{
 		{
 			auto obj = sc->selectionObjectsView.newObject;
 			auto obj = sc->selectionObjectsView.newObject;
 			controller->commitObjectCreate(sc->level);
 			controller->commitObjectCreate(sc->level);