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();
@@ -110,8 +115,6 @@ private slots:
void on_actionUpdate_appearance_triggered();
void on_actionRecreate_obstacles_triggered();
void switchDefaultPlayer(const PlayerColor &);
void on_actionCut_triggered();
@@ -169,8 +172,6 @@ private:
void preparePreview(const QModelIndex & index);
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,12 +36,13 @@
#include "mainwindow.h"
#include "inspector/inspector.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)
{
for(int i : {0, 1})
@@ -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)
{
@@ -581,24 +605,23 @@ bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString
if(!mod.second.parent.empty())
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);
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);