1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-25 22:42:04 +02:00

PlayerSelectionDialog implementation + code cleanup

This commit is contained in:
MichalZr6
2025-03-25 15:39:42 +01:00
parent 87326ce9d9
commit 9964545e7a
7 changed files with 284 additions and 51 deletions

View File

@@ -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

View File

@@ -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();
});
}

View File

@@ -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);
};

View File

@@ -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); explicit MainWindow(QWidget *parent = nullptr);
~MainWindow(); ~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();
@@ -111,8 +116,6 @@ private slots:
void on_actionRecreate_obstacles_triggered(); void on_actionRecreate_obstacles_triggered();
void switchDefaultPlayer(const PlayerColor &);
void on_actionCut_triggered(); void on_actionCut_triggered();
void on_actionCopy_triggered(); void on_actionCopy_triggered();
@@ -170,8 +173,6 @@ private:
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;

View File

@@ -36,6 +36,7 @@
#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) MapController::MapController(QObject * parent)
: QObject(parent) : QObject(parent)
@@ -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);
//find all objects of such type
int objCounter = 0;
for(auto o : _map->objects)
{
if(o->ID == newObj->ID && o->subID == newObj->subID)
{
++objCounter;
}
}
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)) 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)
{ {
error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName)); if(o->ID == grailObj->ID && o->subID == grailObj->subID)
return false; {
++objCounter;
}
} }
// check if object's mod is in required mods in map settings if(objCounter >= 1)
{
error = QObject::tr("There can only be one grail object on the map.");
return false; //maplimit reached
}
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)
{
main->switchDefaultPlayer(dialog.getSelectedPlayer());
return true;
}
error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(heroObj->instanceName));
return false;
}
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)
{ {
@@ -582,23 +606,22 @@ bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString
submod = " (" + tr("submod of") + " " + QString::fromStdString(mod.second.parent) + ")"; submod = " (" + tr("submod of") + " " + QString::fromStdString(mod.second.parent) + ")";
auto reply = QMessageBox::question(main, auto reply = QMessageBox::question(main,
tr("Required Mod Missing"), tr("Missing Required Mod"),
tr("This object is from the mod '%1'%2.\n" tr("This object is from the mod '%1'%2.\n\n"
"The mod is currently not in the map's required modifications list.\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 ?") "Do you want to add this mod to the required modifications ?\n")
.arg(QString::fromStdString(LIBRARY->modh->getModInfo(mod.first).getVerificationInfo().name), submod), .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)

View File

@@ -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);

View File

@@ -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);