editor_window.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. #include "editor_window.h"
  2. #include "json_edit_dialog.h"
  3. #include "resize_dialog.h"
  4. #include <QAction>
  5. #include <QCloseEvent>
  6. #include <QFileDialog>
  7. #include <QFileInfo>
  8. #include <QHBoxLayout>
  9. #include <QJsonArray>
  10. #include <QJsonDocument>
  11. #include <QJsonObject>
  12. #include <QMenuBar>
  13. #include <QMessageBox>
  14. #include <QSplitter>
  15. #include <QStatusBar>
  16. #include <QToolBar>
  17. #include <QVBoxLayout>
  18. namespace MapEditor {
  19. EditorWindow::EditorWindow(QWidget *parent) : QMainWindow(parent) {
  20. m_mapData = new MapData(this);
  21. setupUI();
  22. setupMenus();
  23. connect(m_mapData, &MapData::modifiedChanged, this,
  24. &EditorWindow::onModifiedChanged);
  25. connect(m_mapData, &MapData::undoRedoChanged, this,
  26. &EditorWindow::onUndoRedoChanged);
  27. connect(m_mapData, &MapData::dataChanged, this,
  28. &EditorWindow::updateDimensionsLabel);
  29. setWindowTitle("Standard of Iron - Map Editor");
  30. resize(1400, 900);
  31. newMap();
  32. }
  33. EditorWindow::~EditorWindow() = default;
  34. void EditorWindow::setupUI() {
  35. auto *centralWidget = new QWidget(this);
  36. setCentralWidget(centralWidget);
  37. auto *mainLayout = new QHBoxLayout(centralWidget);
  38. mainLayout->setContentsMargins(0, 0, 0, 0);
  39. mainLayout->setSpacing(0);
  40. m_toolPanel = new ToolPanel(this);
  41. connect(m_toolPanel, &ToolPanel::toolSelected, this,
  42. &EditorWindow::onToolSelected);
  43. m_canvas = new MapCanvas(this);
  44. m_canvas->setMapData(m_mapData);
  45. connect(m_canvas, &MapCanvas::elementDoubleClicked, this,
  46. &EditorWindow::onElementDoubleClicked);
  47. connect(m_canvas, &MapCanvas::gridDoubleClicked, this,
  48. &EditorWindow::onGridDoubleClicked);
  49. connect(m_canvas, &MapCanvas::toolCleared, this,
  50. &EditorWindow::onToolCleared);
  51. auto *splitter = new QSplitter(Qt::Horizontal, this);
  52. splitter->addWidget(m_toolPanel);
  53. splitter->addWidget(m_canvas);
  54. splitter->setStretchFactor(0, 0);
  55. splitter->setStretchFactor(1, 1);
  56. mainLayout->addWidget(splitter);
  57. m_statusLabel = new QLabel("Ready", this);
  58. m_dimensionsLabel = new QLabel("", this);
  59. m_dimensionsLabel->setToolTip(
  60. "Double-click on empty canvas area to edit dimensions");
  61. statusBar()->addWidget(m_statusLabel);
  62. statusBar()->addPermanentWidget(m_dimensionsLabel);
  63. }
  64. void EditorWindow::setupMenus() {
  65. auto *fileMenu = menuBar()->addMenu("&File");
  66. auto *newAction = new QAction("&New", this);
  67. newAction->setShortcut(QKeySequence::New);
  68. connect(newAction, &QAction::triggered, this, &EditorWindow::newMap);
  69. fileMenu->addAction(newAction);
  70. auto *openAction = new QAction("&Open...", this);
  71. openAction->setShortcut(QKeySequence::Open);
  72. connect(openAction, &QAction::triggered, this, &EditorWindow::openMap);
  73. fileMenu->addAction(openAction);
  74. fileMenu->addSeparator();
  75. auto *saveAction = new QAction("&Save", this);
  76. saveAction->setShortcut(QKeySequence::Save);
  77. connect(saveAction, &QAction::triggered, this, &EditorWindow::saveMap);
  78. fileMenu->addAction(saveAction);
  79. auto *saveAsAction = new QAction("Save &As...", this);
  80. saveAsAction->setShortcut(QKeySequence::SaveAs);
  81. connect(saveAsAction, &QAction::triggered, this, &EditorWindow::saveMapAs);
  82. fileMenu->addAction(saveAsAction);
  83. fileMenu->addSeparator();
  84. auto *exitAction = new QAction("E&xit", this);
  85. exitAction->setShortcut(QKeySequence::Quit);
  86. connect(exitAction, &QAction::triggered, this, &QWidget::close);
  87. fileMenu->addAction(exitAction);
  88. auto *editMenu = menuBar()->addMenu("&Edit");
  89. m_undoAction = new QAction("&Undo", this);
  90. m_undoAction->setShortcut(QKeySequence::Undo);
  91. m_undoAction->setEnabled(false);
  92. connect(m_undoAction, &QAction::triggered, this, &EditorWindow::undo);
  93. editMenu->addAction(m_undoAction);
  94. m_redoAction = new QAction("&Redo", this);
  95. m_redoAction->setShortcut(QKeySequence::Redo);
  96. m_redoAction->setEnabled(false);
  97. connect(m_redoAction, &QAction::triggered, this, &EditorWindow::redo);
  98. editMenu->addAction(m_redoAction);
  99. editMenu->addSeparator();
  100. auto *resizeAction = new QAction("&Resize Map...", this);
  101. connect(resizeAction, &QAction::triggered, this, &EditorWindow::resizeMap);
  102. editMenu->addAction(resizeAction);
  103. auto *toolbar = addToolBar("Main");
  104. toolbar->addAction(newAction);
  105. toolbar->addAction(openAction);
  106. toolbar->addAction(saveAction);
  107. toolbar->addSeparator();
  108. toolbar->addAction(m_undoAction);
  109. toolbar->addAction(m_redoAction);
  110. toolbar->addSeparator();
  111. toolbar->addAction(resizeAction);
  112. }
  113. void EditorWindow::newMap() {
  114. if (!maybeSave()) {
  115. return;
  116. }
  117. m_mapData->clear();
  118. m_currentFilePath.clear();
  119. updateWindowTitle();
  120. m_statusLabel->setText("New map created");
  121. }
  122. void EditorWindow::openMap() {
  123. if (!maybeSave()) {
  124. return;
  125. }
  126. QString filePath = QFileDialog::getOpenFileName(
  127. this, "Open Map", QString(), "JSON Files (*.json);;All Files (*)");
  128. if (filePath.isEmpty()) {
  129. return;
  130. }
  131. if (m_mapData->loadFromJson(filePath)) {
  132. m_currentFilePath = filePath;
  133. updateWindowTitle();
  134. m_statusLabel->setText("Loaded: " + filePath);
  135. } else {
  136. QMessageBox::critical(this, "Error",
  137. "Failed to load map file: " + filePath);
  138. }
  139. }
  140. bool EditorWindow::loadFile(const QString &filePath) {
  141. if (m_mapData->loadFromJson(filePath)) {
  142. m_currentFilePath = filePath;
  143. updateWindowTitle();
  144. m_statusLabel->setText("Loaded: " + filePath);
  145. return true;
  146. }
  147. QMessageBox::critical(this, "Error", "Failed to load map file: " + filePath);
  148. return false;
  149. }
  150. void EditorWindow::saveMap() {
  151. if (m_currentFilePath.isEmpty()) {
  152. saveMapAs();
  153. } else {
  154. if (m_mapData->saveToJson(m_currentFilePath)) {
  155. m_mapData->setModified(false);
  156. m_statusLabel->setText("Saved: " + m_currentFilePath);
  157. } else {
  158. QMessageBox::critical(this, "Error",
  159. "Failed to save map file: " + m_currentFilePath);
  160. }
  161. }
  162. }
  163. void EditorWindow::saveMapAs() {
  164. QString filePath = QFileDialog::getSaveFileName(
  165. this, "Save Map As", QString(), "JSON Files (*.json);;All Files (*)");
  166. if (filePath.isEmpty()) {
  167. return;
  168. }
  169. if (!filePath.endsWith(".json", Qt::CaseInsensitive)) {
  170. filePath += ".json";
  171. }
  172. if (m_mapData->saveToJson(filePath)) {
  173. m_currentFilePath = filePath;
  174. m_mapData->setModified(false);
  175. updateWindowTitle();
  176. m_statusLabel->setText("Saved: " + filePath);
  177. } else {
  178. QMessageBox::critical(this, "Error",
  179. "Failed to save map file: " + filePath);
  180. }
  181. }
  182. void EditorWindow::resizeMap() {
  183. const GridSettings &grid = m_mapData->grid();
  184. ResizeDialog dialog(grid.width, grid.height, this);
  185. if (dialog.exec() == QDialog::Accepted) {
  186. GridSettings newGrid = grid;
  187. newGrid.width = dialog.newWidth();
  188. newGrid.height = dialog.newHeight();
  189. m_mapData->setGrid(newGrid);
  190. m_canvas->update();
  191. m_statusLabel->setText(
  192. QString("Map resized to %1x%2").arg(newGrid.width).arg(newGrid.height));
  193. }
  194. }
  195. void EditorWindow::undo() {
  196. m_mapData->undo();
  197. m_statusLabel->setText("Undo");
  198. }
  199. void EditorWindow::redo() {
  200. m_mapData->redo();
  201. m_statusLabel->setText("Redo");
  202. }
  203. void EditorWindow::onToolSelected(ToolType tool) {
  204. m_canvas->setCurrentTool(tool);
  205. QString toolName;
  206. switch (tool) {
  207. case ToolType::Select:
  208. toolName = "Select";
  209. break;
  210. case ToolType::Hill:
  211. toolName = "Hill";
  212. break;
  213. case ToolType::Mountain:
  214. toolName = "Mountain";
  215. break;
  216. case ToolType::River:
  217. toolName = "River (click start, then end)";
  218. break;
  219. case ToolType::Road:
  220. toolName = "Road (click start, then end)";
  221. break;
  222. case ToolType::Bridge:
  223. toolName = "Bridge (click start, then end)";
  224. break;
  225. case ToolType::Firecamp:
  226. toolName = "Firecamp";
  227. break;
  228. case ToolType::Barracks:
  229. toolName = "Barracks (assign to team)";
  230. break;
  231. case ToolType::Village:
  232. toolName = "Village (assign to team)";
  233. break;
  234. case ToolType::Eraser:
  235. toolName = "Eraser";
  236. break;
  237. }
  238. m_statusLabel->setText("Tool: " + toolName);
  239. }
  240. void EditorWindow::onToolCleared() {
  241. m_toolPanel->clearSelection();
  242. m_statusLabel->setText("Tool: Select");
  243. }
  244. void EditorWindow::onGridDoubleClicked() { resizeMap(); }
  245. void EditorWindow::onUndoRedoChanged() {
  246. m_undoAction->setEnabled(m_mapData->canUndo());
  247. m_redoAction->setEnabled(m_mapData->canRedo());
  248. }
  249. void EditorWindow::updateDimensionsLabel() {
  250. const GridSettings &grid = m_mapData->grid();
  251. m_dimensionsLabel->setText(
  252. QString("Map: %1 x %2").arg(grid.width).arg(grid.height));
  253. }
  254. void EditorWindow::onElementDoubleClicked(int elementType, int index) {
  255. QJsonObject json;
  256. QString title;
  257. if (elementType == 0) {
  258. const auto &terrain = m_mapData->terrainElements();
  259. if (index < 0 || index >= terrain.size()) {
  260. return;
  261. }
  262. const auto &elem = terrain[index];
  263. json["type"] = elem.type;
  264. json["x"] = static_cast<double>(elem.x);
  265. json["z"] = static_cast<double>(elem.z);
  266. json["radius"] = static_cast<double>(elem.radius);
  267. json["width"] = static_cast<double>(elem.width);
  268. json["depth"] = static_cast<double>(elem.depth);
  269. json["height"] = static_cast<double>(elem.height);
  270. json["rotation"] = static_cast<double>(elem.rotation);
  271. if (!elem.entrances.isEmpty()) {
  272. json["entrances"] = elem.entrances;
  273. }
  274. for (const QString &key : elem.extraFields.keys()) {
  275. json[key] = elem.extraFields[key];
  276. }
  277. title = "Edit Terrain: " + elem.type;
  278. } else if (elementType == 1) {
  279. const auto &firecamps = m_mapData->firecamps();
  280. if (index < 0 || index >= firecamps.size()) {
  281. return;
  282. }
  283. const auto &elem = firecamps[index];
  284. json["x"] = static_cast<double>(elem.x);
  285. json["z"] = static_cast<double>(elem.z);
  286. json["intensity"] = static_cast<double>(elem.intensity);
  287. json["radius"] = static_cast<double>(elem.radius);
  288. for (const QString &key : elem.extraFields.keys()) {
  289. json[key] = elem.extraFields[key];
  290. }
  291. title = "Edit Firecamp";
  292. } else if (elementType == 2) {
  293. const auto &linear = m_mapData->linearElements();
  294. if (index < 0 || index >= linear.size()) {
  295. return;
  296. }
  297. const auto &elem = linear[index];
  298. json["type"] = elem.type;
  299. json["start"] = QJsonArray{static_cast<double>(elem.start.x()),
  300. static_cast<double>(elem.start.y())};
  301. json["end"] = QJsonArray{static_cast<double>(elem.end.x()),
  302. static_cast<double>(elem.end.y())};
  303. json["width"] = static_cast<double>(elem.width);
  304. if (elem.type == "bridge") {
  305. json["height"] = static_cast<double>(elem.height);
  306. }
  307. if (elem.type == "road" && !elem.style.isEmpty()) {
  308. json["style"] = elem.style;
  309. }
  310. for (const QString &key : elem.extraFields.keys()) {
  311. json[key] = elem.extraFields[key];
  312. }
  313. title = "Edit " + elem.type;
  314. } else if (elementType == 3) {
  315. const auto &structures = m_mapData->structures();
  316. if (index < 0 || index >= structures.size()) {
  317. return;
  318. }
  319. const auto &elem = structures[index];
  320. json["type"] = elem.type;
  321. json["x"] = static_cast<double>(elem.x);
  322. json["z"] = static_cast<double>(elem.z);
  323. json["playerId"] = elem.playerId;
  324. json["maxPopulation"] = elem.maxPopulation;
  325. if (!elem.nation.isEmpty()) {
  326. json["nation"] = elem.nation;
  327. }
  328. for (const QString &key : elem.extraFields.keys()) {
  329. json[key] = elem.extraFields[key];
  330. }
  331. title = "Edit " + elem.type;
  332. } else {
  333. return;
  334. }
  335. JsonEditDialog dialog(title, json, this);
  336. if (dialog.exec() == QDialog::Accepted && dialog.isValid()) {
  337. QJsonObject newJson = dialog.getJson();
  338. if (elementType == 0) {
  339. TerrainElement elem;
  340. elem.type = newJson["type"].toString();
  341. elem.x = static_cast<float>(newJson["x"].toDouble());
  342. elem.z = static_cast<float>(newJson["z"].toDouble());
  343. elem.radius = static_cast<float>(newJson["radius"].toDouble(10.0));
  344. elem.width = static_cast<float>(newJson["width"].toDouble(0.0));
  345. elem.depth = static_cast<float>(newJson["depth"].toDouble(0.0));
  346. elem.height = static_cast<float>(newJson["height"].toDouble(3.0));
  347. elem.rotation = static_cast<float>(newJson["rotation"].toDouble(0.0));
  348. elem.entrances = newJson["entrances"].toArray();
  349. QStringList knownKeys = {"type", "x", "z",
  350. "radius", "width", "depth",
  351. "height", "rotation", "entrances"};
  352. for (const QString &key : newJson.keys()) {
  353. if (!knownKeys.contains(key)) {
  354. elem.extraFields[key] = newJson[key];
  355. }
  356. }
  357. m_mapData->updateTerrainElement(index, elem);
  358. } else if (elementType == 1) {
  359. FirecampElement elem;
  360. elem.x = static_cast<float>(newJson["x"].toDouble());
  361. elem.z = static_cast<float>(newJson["z"].toDouble());
  362. elem.intensity = static_cast<float>(newJson["intensity"].toDouble(1.0));
  363. elem.radius = static_cast<float>(newJson["radius"].toDouble(3.0));
  364. QStringList knownKeys = {"x", "z", "intensity", "radius"};
  365. for (const QString &key : newJson.keys()) {
  366. if (!knownKeys.contains(key)) {
  367. elem.extraFields[key] = newJson[key];
  368. }
  369. }
  370. m_mapData->updateFirecamp(index, elem);
  371. } else if (elementType == 2) {
  372. LinearElement elem;
  373. elem.type = newJson["type"].toString();
  374. QJsonArray startArr = newJson["start"].toArray();
  375. QJsonArray endArr = newJson["end"].toArray();
  376. if (startArr.size() >= 2 && endArr.size() >= 2) {
  377. elem.start = QVector2D(static_cast<float>(startArr[0].toDouble()),
  378. static_cast<float>(startArr[1].toDouble()));
  379. elem.end = QVector2D(static_cast<float>(endArr[0].toDouble()),
  380. static_cast<float>(endArr[1].toDouble()));
  381. }
  382. elem.width = static_cast<float>(newJson["width"].toDouble(3.0));
  383. elem.height = static_cast<float>(newJson["height"].toDouble(0.5));
  384. elem.style = newJson["style"].toString("default");
  385. QStringList knownKeys = {"type", "start", "end",
  386. "width", "height", "style"};
  387. for (const QString &key : newJson.keys()) {
  388. if (!knownKeys.contains(key)) {
  389. elem.extraFields[key] = newJson[key];
  390. }
  391. }
  392. m_mapData->updateLinearElement(index, elem);
  393. } else if (elementType == 3) {
  394. StructureElement elem;
  395. elem.type = newJson["type"].toString();
  396. elem.x = static_cast<float>(newJson["x"].toDouble());
  397. elem.z = static_cast<float>(newJson["z"].toDouble());
  398. elem.playerId = newJson["playerId"].toInt(0);
  399. elem.maxPopulation = newJson["maxPopulation"].toInt(150);
  400. elem.nation = newJson["nation"].toString();
  401. QStringList knownKeys = {"type", "x", "z", "playerId",
  402. "maxPopulation", "nation"};
  403. for (const QString &key : newJson.keys()) {
  404. if (!knownKeys.contains(key)) {
  405. elem.extraFields[key] = newJson[key];
  406. }
  407. }
  408. m_mapData->updateStructure(index, elem);
  409. }
  410. }
  411. }
  412. void EditorWindow::onModifiedChanged(bool modified) {
  413. Q_UNUSED(modified)
  414. updateWindowTitle();
  415. }
  416. void EditorWindow::updateWindowTitle() {
  417. QString title = "Standard of Iron - Map Editor";
  418. if (!m_currentFilePath.isEmpty()) {
  419. title += " - " + QFileInfo(m_currentFilePath).fileName();
  420. } else {
  421. title += " - " + m_mapData->name();
  422. }
  423. if (m_mapData->isModified()) {
  424. title += " *";
  425. }
  426. setWindowTitle(title);
  427. }
  428. bool EditorWindow::maybeSave() {
  429. if (!m_mapData->isModified()) {
  430. return true;
  431. }
  432. QMessageBox::StandardButton ret = QMessageBox::warning(
  433. this, "Unsaved Changes",
  434. "The map has been modified.\nDo you want to save your changes?",
  435. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
  436. if (ret == QMessageBox::Save) {
  437. saveMap();
  438. return !m_mapData->isModified();
  439. }
  440. if (ret == QMessageBox::Cancel) {
  441. return false;
  442. }
  443. return true;
  444. }
  445. void EditorWindow::closeEvent(QCloseEvent *event) {
  446. if (maybeSave()) {
  447. event->accept();
  448. } else {
  449. event->ignore();
  450. }
  451. }
  452. } // namespace MapEditor