#include "editor_window.h" #include "json_edit_dialog.h" #include "resize_dialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MapEditor { EditorWindow::EditorWindow(QWidget *parent) : QMainWindow(parent) { m_mapData = new MapData(this); setupUI(); setupMenus(); connect(m_mapData, &MapData::modifiedChanged, this, &EditorWindow::onModifiedChanged); connect(m_mapData, &MapData::undoRedoChanged, this, &EditorWindow::onUndoRedoChanged); connect(m_mapData, &MapData::dataChanged, this, &EditorWindow::updateDimensionsLabel); setWindowTitle("Standard of Iron - Map Editor"); resize(1400, 900); newMap(); } EditorWindow::~EditorWindow() = default; void EditorWindow::setupUI() { auto *centralWidget = new QWidget(this); setCentralWidget(centralWidget); auto *mainLayout = new QHBoxLayout(centralWidget); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setSpacing(0); m_toolPanel = new ToolPanel(this); connect(m_toolPanel, &ToolPanel::toolSelected, this, &EditorWindow::onToolSelected); m_canvas = new MapCanvas(this); m_canvas->setMapData(m_mapData); connect(m_canvas, &MapCanvas::elementDoubleClicked, this, &EditorWindow::onElementDoubleClicked); connect(m_canvas, &MapCanvas::gridDoubleClicked, this, &EditorWindow::onGridDoubleClicked); connect(m_canvas, &MapCanvas::toolCleared, this, &EditorWindow::onToolCleared); auto *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(m_toolPanel); splitter->addWidget(m_canvas); splitter->setStretchFactor(0, 0); splitter->setStretchFactor(1, 1); mainLayout->addWidget(splitter); m_statusLabel = new QLabel("Ready", this); m_dimensionsLabel = new QLabel("", this); m_dimensionsLabel->setToolTip( "Double-click on empty canvas area to edit dimensions"); statusBar()->addWidget(m_statusLabel); statusBar()->addPermanentWidget(m_dimensionsLabel); } void EditorWindow::setupMenus() { auto *fileMenu = menuBar()->addMenu("&File"); auto *newAction = new QAction("&New", this); newAction->setShortcut(QKeySequence::New); connect(newAction, &QAction::triggered, this, &EditorWindow::newMap); fileMenu->addAction(newAction); auto *openAction = new QAction("&Open...", this); openAction->setShortcut(QKeySequence::Open); connect(openAction, &QAction::triggered, this, &EditorWindow::openMap); fileMenu->addAction(openAction); fileMenu->addSeparator(); auto *saveAction = new QAction("&Save", this); saveAction->setShortcut(QKeySequence::Save); connect(saveAction, &QAction::triggered, this, &EditorWindow::saveMap); fileMenu->addAction(saveAction); auto *saveAsAction = new QAction("Save &As...", this); saveAsAction->setShortcut(QKeySequence::SaveAs); connect(saveAsAction, &QAction::triggered, this, &EditorWindow::saveMapAs); fileMenu->addAction(saveAsAction); fileMenu->addSeparator(); auto *exitAction = new QAction("E&xit", this); exitAction->setShortcut(QKeySequence::Quit); connect(exitAction, &QAction::triggered, this, &QWidget::close); fileMenu->addAction(exitAction); auto *editMenu = menuBar()->addMenu("&Edit"); m_undoAction = new QAction("&Undo", this); m_undoAction->setShortcut(QKeySequence::Undo); m_undoAction->setEnabled(false); connect(m_undoAction, &QAction::triggered, this, &EditorWindow::undo); editMenu->addAction(m_undoAction); m_redoAction = new QAction("&Redo", this); m_redoAction->setShortcut(QKeySequence::Redo); m_redoAction->setEnabled(false); connect(m_redoAction, &QAction::triggered, this, &EditorWindow::redo); editMenu->addAction(m_redoAction); editMenu->addSeparator(); auto *resizeAction = new QAction("&Resize Map...", this); connect(resizeAction, &QAction::triggered, this, &EditorWindow::resizeMap); editMenu->addAction(resizeAction); auto *toolbar = addToolBar("Main"); toolbar->addAction(newAction); toolbar->addAction(openAction); toolbar->addAction(saveAction); toolbar->addSeparator(); toolbar->addAction(m_undoAction); toolbar->addAction(m_redoAction); toolbar->addSeparator(); toolbar->addAction(resizeAction); } void EditorWindow::newMap() { if (!maybeSave()) { return; } m_mapData->clear(); m_currentFilePath.clear(); updateWindowTitle(); m_statusLabel->setText("New map created"); } void EditorWindow::openMap() { if (!maybeSave()) { return; } QString filePath = QFileDialog::getOpenFileName( this, "Open Map", QString(), "JSON Files (*.json);;All Files (*)"); if (filePath.isEmpty()) { return; } if (m_mapData->loadFromJson(filePath)) { m_currentFilePath = filePath; updateWindowTitle(); m_statusLabel->setText("Loaded: " + filePath); } else { QMessageBox::critical(this, "Error", "Failed to load map file: " + filePath); } } bool EditorWindow::loadFile(const QString &filePath) { if (m_mapData->loadFromJson(filePath)) { m_currentFilePath = filePath; updateWindowTitle(); m_statusLabel->setText("Loaded: " + filePath); return true; } QMessageBox::critical(this, "Error", "Failed to load map file: " + filePath); return false; } void EditorWindow::saveMap() { if (m_currentFilePath.isEmpty()) { saveMapAs(); } else { if (m_mapData->saveToJson(m_currentFilePath)) { m_mapData->setModified(false); m_statusLabel->setText("Saved: " + m_currentFilePath); } else { QMessageBox::critical(this, "Error", "Failed to save map file: " + m_currentFilePath); } } } void EditorWindow::saveMapAs() { QString filePath = QFileDialog::getSaveFileName( this, "Save Map As", QString(), "JSON Files (*.json);;All Files (*)"); if (filePath.isEmpty()) { return; } if (!filePath.endsWith(".json", Qt::CaseInsensitive)) { filePath += ".json"; } if (m_mapData->saveToJson(filePath)) { m_currentFilePath = filePath; m_mapData->setModified(false); updateWindowTitle(); m_statusLabel->setText("Saved: " + filePath); } else { QMessageBox::critical(this, "Error", "Failed to save map file: " + filePath); } } void EditorWindow::resizeMap() { const GridSettings &grid = m_mapData->grid(); ResizeDialog dialog(grid.width, grid.height, this); if (dialog.exec() == QDialog::Accepted) { GridSettings newGrid = grid; newGrid.width = dialog.newWidth(); newGrid.height = dialog.newHeight(); m_mapData->setGrid(newGrid); m_canvas->update(); m_statusLabel->setText( QString("Map resized to %1x%2").arg(newGrid.width).arg(newGrid.height)); } } void EditorWindow::undo() { m_mapData->undo(); m_statusLabel->setText("Undo"); } void EditorWindow::redo() { m_mapData->redo(); m_statusLabel->setText("Redo"); } void EditorWindow::onToolSelected(ToolType tool) { m_canvas->setCurrentTool(tool); QString toolName; switch (tool) { case ToolType::Select: toolName = "Select"; break; case ToolType::Hill: toolName = "Hill"; break; case ToolType::Mountain: toolName = "Mountain"; break; case ToolType::River: toolName = "River (click start, then end)"; break; case ToolType::Road: toolName = "Road (click start, then end)"; break; case ToolType::Bridge: toolName = "Bridge (click start, then end)"; break; case ToolType::Firecamp: toolName = "Firecamp"; break; case ToolType::Barracks: toolName = "Barracks (assign to team)"; break; case ToolType::Village: toolName = "Village (assign to team)"; break; case ToolType::Eraser: toolName = "Eraser"; break; } m_statusLabel->setText("Tool: " + toolName); } void EditorWindow::onToolCleared() { m_toolPanel->clearSelection(); m_statusLabel->setText("Tool: Select"); } void EditorWindow::onGridDoubleClicked() { resizeMap(); } void EditorWindow::onUndoRedoChanged() { m_undoAction->setEnabled(m_mapData->canUndo()); m_redoAction->setEnabled(m_mapData->canRedo()); } void EditorWindow::updateDimensionsLabel() { const GridSettings &grid = m_mapData->grid(); m_dimensionsLabel->setText( QString("Map: %1 x %2").arg(grid.width).arg(grid.height)); } void EditorWindow::onElementDoubleClicked(int elementType, int index) { QJsonObject json; QString title; if (elementType == 0) { const auto &terrain = m_mapData->terrainElements(); if (index < 0 || index >= terrain.size()) { return; } const auto &elem = terrain[index]; json["type"] = elem.type; json["x"] = static_cast(elem.x); json["z"] = static_cast(elem.z); json["radius"] = static_cast(elem.radius); json["width"] = static_cast(elem.width); json["depth"] = static_cast(elem.depth); json["height"] = static_cast(elem.height); json["rotation"] = static_cast(elem.rotation); if (!elem.entrances.isEmpty()) { json["entrances"] = elem.entrances; } for (const QString &key : elem.extraFields.keys()) { json[key] = elem.extraFields[key]; } title = "Edit Terrain: " + elem.type; } else if (elementType == 1) { const auto &firecamps = m_mapData->firecamps(); if (index < 0 || index >= firecamps.size()) { return; } const auto &elem = firecamps[index]; json["x"] = static_cast(elem.x); json["z"] = static_cast(elem.z); json["intensity"] = static_cast(elem.intensity); json["radius"] = static_cast(elem.radius); for (const QString &key : elem.extraFields.keys()) { json[key] = elem.extraFields[key]; } title = "Edit Firecamp"; } else if (elementType == 2) { const auto &linear = m_mapData->linearElements(); if (index < 0 || index >= linear.size()) { return; } const auto &elem = linear[index]; json["type"] = elem.type; json["start"] = QJsonArray{static_cast(elem.start.x()), static_cast(elem.start.y())}; json["end"] = QJsonArray{static_cast(elem.end.x()), static_cast(elem.end.y())}; json["width"] = static_cast(elem.width); if (elem.type == "bridge") { json["height"] = static_cast(elem.height); } if (elem.type == "road" && !elem.style.isEmpty()) { json["style"] = elem.style; } for (const QString &key : elem.extraFields.keys()) { json[key] = elem.extraFields[key]; } title = "Edit " + elem.type; } else if (elementType == 3) { const auto &structures = m_mapData->structures(); if (index < 0 || index >= structures.size()) { return; } const auto &elem = structures[index]; json["type"] = elem.type; json["x"] = static_cast(elem.x); json["z"] = static_cast(elem.z); json["playerId"] = elem.playerId; json["maxPopulation"] = elem.maxPopulation; if (!elem.nation.isEmpty()) { json["nation"] = elem.nation; } for (const QString &key : elem.extraFields.keys()) { json[key] = elem.extraFields[key]; } title = "Edit " + elem.type; } else { return; } JsonEditDialog dialog(title, json, this); if (dialog.exec() == QDialog::Accepted && dialog.isValid()) { QJsonObject newJson = dialog.getJson(); if (elementType == 0) { TerrainElement elem; elem.type = newJson["type"].toString(); elem.x = static_cast(newJson["x"].toDouble()); elem.z = static_cast(newJson["z"].toDouble()); elem.radius = static_cast(newJson["radius"].toDouble(10.0)); elem.width = static_cast(newJson["width"].toDouble(0.0)); elem.depth = static_cast(newJson["depth"].toDouble(0.0)); elem.height = static_cast(newJson["height"].toDouble(3.0)); elem.rotation = static_cast(newJson["rotation"].toDouble(0.0)); elem.entrances = newJson["entrances"].toArray(); QStringList knownKeys = {"type", "x", "z", "radius", "width", "depth", "height", "rotation", "entrances"}; for (const QString &key : newJson.keys()) { if (!knownKeys.contains(key)) { elem.extraFields[key] = newJson[key]; } } m_mapData->updateTerrainElement(index, elem); } else if (elementType == 1) { FirecampElement elem; elem.x = static_cast(newJson["x"].toDouble()); elem.z = static_cast(newJson["z"].toDouble()); elem.intensity = static_cast(newJson["intensity"].toDouble(1.0)); elem.radius = static_cast(newJson["radius"].toDouble(3.0)); QStringList knownKeys = {"x", "z", "intensity", "radius"}; for (const QString &key : newJson.keys()) { if (!knownKeys.contains(key)) { elem.extraFields[key] = newJson[key]; } } m_mapData->updateFirecamp(index, elem); } else if (elementType == 2) { LinearElement elem; elem.type = newJson["type"].toString(); QJsonArray startArr = newJson["start"].toArray(); QJsonArray endArr = newJson["end"].toArray(); if (startArr.size() >= 2 && endArr.size() >= 2) { elem.start = QVector2D(static_cast(startArr[0].toDouble()), static_cast(startArr[1].toDouble())); elem.end = QVector2D(static_cast(endArr[0].toDouble()), static_cast(endArr[1].toDouble())); } elem.width = static_cast(newJson["width"].toDouble(3.0)); elem.height = static_cast(newJson["height"].toDouble(0.5)); elem.style = newJson["style"].toString("default"); QStringList knownKeys = {"type", "start", "end", "width", "height", "style"}; for (const QString &key : newJson.keys()) { if (!knownKeys.contains(key)) { elem.extraFields[key] = newJson[key]; } } m_mapData->updateLinearElement(index, elem); } else if (elementType == 3) { StructureElement elem; elem.type = newJson["type"].toString(); elem.x = static_cast(newJson["x"].toDouble()); elem.z = static_cast(newJson["z"].toDouble()); elem.playerId = newJson["playerId"].toInt(0); elem.maxPopulation = newJson["maxPopulation"].toInt(150); elem.nation = newJson["nation"].toString(); QStringList knownKeys = {"type", "x", "z", "playerId", "maxPopulation", "nation"}; for (const QString &key : newJson.keys()) { if (!knownKeys.contains(key)) { elem.extraFields[key] = newJson[key]; } } m_mapData->updateStructure(index, elem); } } } void EditorWindow::onModifiedChanged(bool modified) { Q_UNUSED(modified) updateWindowTitle(); } void EditorWindow::updateWindowTitle() { QString title = "Standard of Iron - Map Editor"; if (!m_currentFilePath.isEmpty()) { title += " - " + QFileInfo(m_currentFilePath).fileName(); } else { title += " - " + m_mapData->name(); } if (m_mapData->isModified()) { title += " *"; } setWindowTitle(title); } bool EditorWindow::maybeSave() { if (!m_mapData->isModified()) { return true; } QMessageBox::StandardButton ret = QMessageBox::warning( this, "Unsaved Changes", "The map has been modified.\nDo you want to save your changes?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Save) { saveMap(); return !m_mapData->isModified(); } if (ret == QMessageBox::Cancel) { return false; } return true; } void EditorWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) { event->accept(); } else { event->ignore(); } } } // namespace MapEditor