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/rumorsettings.cpp
mapsettings/translations.cpp
PlayerSelectionDialog.cpp
playersettings.cpp
playerparams.cpp
scenelayer.cpp
@@ -70,6 +71,7 @@ set(editor_HEADERS
mapsettings/eventsettings.h
mapsettings/rumorsettings.h
mapsettings/translations.h
PlayerSelectionDialog.h
playersettings.h
playerparams.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
{
Q_OBJECT
Q_OBJECT
const QString mainWindowSizeSetting = "MainWindow/Size";
const QString mainWindowPositionSetting = "MainWindow/Position";
@@ -42,8 +42,8 @@ class MainWindow : public QMainWindow
std::unique_ptr<CBasicLogConfigurator> logConfig;
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
void initializeMap(bool isNew);
@@ -62,6 +62,11 @@ public:
void loadTranslation();
QAction * getActionPlayer(const PlayerColor &);
public slots:
void switchDefaultPlayer(const PlayerColor &);
private slots:
void on_actionOpen_triggered();
@@ -111,8 +116,6 @@ private slots:
void on_actionRecreate_obstacles_triggered();
void switchDefaultPlayer(const PlayerColor &);
void on_actionCut_triggered();
void on_actionCopy_triggered();
@@ -170,8 +173,6 @@ private:
void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
QAction * getActionPlayer(const PlayerColor &);
void changeBrushState(int idx);
void setTitle();
@@ -187,7 +188,7 @@ private:
void updateRecentMenu(const QString & filenameSelect);
private:
Ui::MainWindow * ui;
Ui::MainWindow * ui;
ObjectBrowserProxyModel * objectBrowser = nullptr;
QGraphicsScene * scenePreview;
MapSettings * mapSettings = nullptr;

View File

@@ -36,6 +36,7 @@
#include "mainwindow.h"
#include "inspector/inspector.h"
#include "GameLibrary.h"
#include "PlayerSelectionDialog.h"
MapController::MapController(QObject * parent)
: QObject(parent)
@@ -371,7 +372,7 @@ void MapController::pasteFromClipboard(int level)
{
auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
QString errorMsg;
if (!canPlaceObject(level, obj.get(), errorMsg))
if(!canPlaceObject(obj.get(), errorMsg))
{
errors.push_back(std::move(errorMsg));
continue;
@@ -542,36 +543,59 @@ void MapController::commitObjectCreate(int level)
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
{
//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
}
}
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)
{
error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName));
return false;
if(o->ID == grailObj->ID && o->subID == grailObj->subID)
{
++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;
modAssessmentObject(newObj, modsInfo);
modAssessmentObject(obj, 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) + ")";
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 ?")
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);
if(reply == QMessageBox::Yes)
requestModsUpdate(modsInfo, true); // emit signal for MapSettings
/* emit */ requestModsUpdate(modsInfo, true); // signal for MapSettings
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 true;
}
@@ -634,7 +657,7 @@ ModCompatibilityInfo MapController::modAssessmentAll()
return result;
}
void MapController::modAssessmentObject(CGObjectInstance * obj, ModCompatibilityInfo & result)
void MapController::modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result)
{
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)
{
auto town = dynamic_cast<CGTownInstance *>(obj);
auto town = dynamic_cast<const CGTownInstance *>(obj);
for(const auto & spellID : town->possibleSpells)
{
if(spellID == SpellID::PRESET)
@@ -682,7 +705,7 @@ void MapController::modAssessmentObject(CGObjectInstance * obj, ModCompatibility
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())
{
if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)

View File

@@ -63,9 +63,16 @@ public:
bool discardObject(int level) 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 modAssessmentMap(const CMap & map);

View File

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