LevelFileDialog.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "EditorDefs.h"
  9. #include "LevelFileDialog.h"
  10. #include <AzFramework/API/ApplicationAPI.h>
  11. // Qt
  12. #include <QMessageBox>
  13. #include <QInputDialog>
  14. // Editor
  15. #include "LevelTreeModel.h"
  16. #include "CryEditDoc.h"
  17. #include "API/ToolsApplicationAPI.h"
  18. AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
  19. #include <ui_LevelFileDialog.h>
  20. AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
  21. static const char lastLoadPathFilename[] = "lastLoadPath.preset";
  22. // Folder in which levels are stored
  23. static const char kLevelsFolder[] = "Levels";
  24. CLevelFileDialog::CLevelFileDialog(bool openDialog, QWidget* parent)
  25. : QDialog(parent)
  26. , m_bOpenDialog(openDialog)
  27. , ui(new Ui::LevelFileDialog())
  28. , m_model(new LevelTreeModel(this))
  29. , m_filterModel(new LevelTreeModelFilter(this))
  30. {
  31. ui->setupUi(this);
  32. ui->treeView->header()->close();
  33. m_filterModel->setSourceModel(m_model);
  34. ui->treeView->setModel(m_filterModel);
  35. ui->treeView->installEventFilter(this);
  36. connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
  37. this, &CLevelFileDialog::OnTreeSelectionChanged);
  38. connect(ui->treeView, &QTreeView::doubleClicked, this, [this]()
  39. {
  40. if (m_bOpenDialog && !IsValidLevelSelected())
  41. {
  42. return;
  43. }
  44. OnOK();
  45. });
  46. connect(ui->filterLineEdit, &QLineEdit::textChanged, this, &CLevelFileDialog::OnFilterChanged);
  47. connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CLevelFileDialog::OnCancel);
  48. connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &CLevelFileDialog::OnOK);
  49. connect(ui->newFolderButton, &QPushButton::clicked, this, &CLevelFileDialog::OnNewFolder);
  50. if (m_bOpenDialog)
  51. {
  52. setWindowTitle(tr("Open Level"));
  53. ui->treeView->expandToDepth(1);
  54. ui->newFolderButton->setVisible(false);
  55. ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Open"));
  56. }
  57. else
  58. {
  59. setWindowTitle(tr("Save Level As "));
  60. ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Save"));
  61. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
  62. // Make the name input the default active field for the save as dialog
  63. // The filter input will still be the default active field for the open dialog
  64. setTabOrder(ui->nameLineEdit, ui->filterLineEdit);
  65. connect(ui->nameLineEdit, &QLineEdit::textChanged, this, &CLevelFileDialog::OnNameChanged);
  66. }
  67. // reject invalid file names
  68. ui->nameLineEdit->setValidator(new QRegExpValidator(QRegExp("^[a-zA-Z0-9_\\-./]*$"), ui->nameLineEdit));
  69. ReloadTree();
  70. LoadLastUsedLevelPath();
  71. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  72. }
  73. CLevelFileDialog::~CLevelFileDialog()
  74. {
  75. }
  76. QString CLevelFileDialog::GetFileName() const
  77. {
  78. return m_fileName;
  79. }
  80. void CLevelFileDialog::OnCancel()
  81. {
  82. close();
  83. }
  84. void CLevelFileDialog::OnOK()
  85. {
  86. QString errorMessage;
  87. if (!ValidateSaveLevelPath(errorMessage))
  88. {
  89. QMessageBox::warning(this, tr("Error"), errorMessage);
  90. return;
  91. }
  92. if (m_bOpenDialog)
  93. {
  94. // For Open button
  95. if (!IsValidLevelSelected())
  96. {
  97. QMessageBox box(this);
  98. box.setText(tr("Please enter a valid level name"));
  99. box.setIcon(QMessageBox::Critical);
  100. box.exec();
  101. return;
  102. }
  103. }
  104. else
  105. {
  106. QString levelPath = GetLevelPath();
  107. if (CFileUtil::PathExists(levelPath) && CheckLevelFolder(levelPath))
  108. {
  109. // there is already a level folder at that location, ask before for overwriting it
  110. QMessageBox box(this);
  111. box.setText(tr("Do you really want to overwrite '%1'?").arg(GetEnteredPath()));
  112. box.setIcon(QMessageBox::Warning);
  113. box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
  114. if (box.exec() != QMessageBox::Yes)
  115. {
  116. return;
  117. }
  118. }
  119. m_fileName = levelPath + "/" + Path::GetFileName(levelPath) + EditorUtils::LevelFile::GetDefaultFileExtension();
  120. }
  121. SaveLastUsedLevelPath();
  122. accept();
  123. }
  124. bool CLevelFileDialog::eventFilter(QObject* watched, QEvent* event)
  125. {
  126. if (event->type() == QEvent::KeyPress) {
  127. auto keyEvent = static_cast<QKeyEvent*>(event);
  128. if (keyEvent->key() == Qt::Key_Return) {
  129. OnOK();
  130. return true;
  131. }
  132. }
  133. return QDialog::eventFilter(watched, event);
  134. }
  135. QString CLevelFileDialog::NameForIndex(const QModelIndex& index) const
  136. {
  137. QStringList tokens;
  138. QModelIndex idx = index;
  139. while (idx.isValid() && idx.parent().isValid()) // the root one doesn't count
  140. {
  141. tokens.push_front(idx.data(Qt::DisplayRole).toString());
  142. idx = idx.parent();
  143. }
  144. QString text = tokens.join('/');
  145. const bool isLevelFolder = index.data(LevelTreeModel::IsLevelFolderRole).toBool();
  146. if (!isLevelFolder && !text.isEmpty())
  147. {
  148. text += "/";
  149. }
  150. return text;
  151. }
  152. bool CLevelFileDialog::IsValidLevelSelected()
  153. {
  154. QString levelPath = GetLevelPath();
  155. m_fileName = GetFileName(levelPath);
  156. QString currentExtension = "." + Path::GetExt(m_fileName);
  157. const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
  158. const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
  159. bool isInvalidFileExtension = (currentExtension != defaultExtension && currentExtension != oldExtension);
  160. if (!isInvalidFileExtension && CFileUtil::FileExists(m_fileName))
  161. {
  162. return true;
  163. }
  164. else
  165. {
  166. return false;
  167. }
  168. }
  169. QString CLevelFileDialog::GetLevelPath() const
  170. {
  171. const QString enteredPath = GetEnteredPath();
  172. const QString levelPath = QString("%1/%2/%3").arg(Path::GetEditingGameDataFolder().c_str()).arg(kLevelsFolder).arg(enteredPath);
  173. return levelPath;
  174. }
  175. QString CLevelFileDialog::GetEnteredPath() const
  176. {
  177. QString enteredPath = ui->nameLineEdit->text();
  178. enteredPath = enteredPath.trimmed();
  179. enteredPath = Path::RemoveBackslash(enteredPath);
  180. return enteredPath;
  181. }
  182. QString CLevelFileDialog::GetFileName(QString levelPath)
  183. {
  184. QStringList levelFiles;
  185. QString fileName;
  186. if (CheckLevelFolder(levelPath, &levelFiles) && levelFiles.size() >= 1)
  187. {
  188. const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
  189. const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
  190. // A level folder was entered. Prefer the .ly/.cry file with the
  191. // folder name, otherwise pick the first one in the list
  192. QString path = Path::GetFileName(levelPath);
  193. QString needle = path + defaultExtension;
  194. auto iter = std::find(levelFiles.begin(), levelFiles.end(), needle);
  195. if (iter != levelFiles.end())
  196. {
  197. fileName = levelPath + "/" + *iter;
  198. }
  199. else
  200. {
  201. needle = path + oldExtension;
  202. iter = std::find(levelFiles.begin(), levelFiles.end(), needle);
  203. if (iter != levelFiles.end())
  204. {
  205. fileName = levelPath + "/" + *iter;
  206. }
  207. else
  208. {
  209. fileName = levelPath + "/" + levelFiles[0];
  210. }
  211. }
  212. }
  213. else
  214. {
  215. // Otherwise try to directly load the specified file (backward compatibility)
  216. fileName = levelPath;
  217. }
  218. return fileName;
  219. }
  220. void CLevelFileDialog::OnTreeSelectionChanged()
  221. {
  222. const QModelIndexList indexes = ui->treeView->selectionModel()->selectedIndexes();
  223. if (!indexes.isEmpty())
  224. {
  225. ui->nameLineEdit->setText(NameForIndex(indexes.first()));
  226. }
  227. }
  228. void CLevelFileDialog::OnNewFolder()
  229. {
  230. const QModelIndexList indexes = ui->treeView->selectionModel()->selectedIndexes();
  231. if (indexes.isEmpty())
  232. {
  233. QMessageBox box(this);
  234. box.setText(tr("Please select a folder first"));
  235. box.setIcon(QMessageBox::Critical);
  236. box.exec();
  237. return;
  238. }
  239. const QModelIndex index = indexes.first();
  240. const bool isLevelFolder = index.data(LevelTreeModel::IsLevelFolderRole).toBool();
  241. // Creating folders is not allowed in level folders
  242. if (!isLevelFolder && index.isValid())
  243. {
  244. const QString parentFullPath = index.data(LevelTreeModel::FullPathRole).toString();
  245. QInputDialog inputDlg(this);
  246. inputDlg.setLabelText(tr("Please select a folder name"));
  247. if (inputDlg.exec() == QDialog::Accepted && !inputDlg.textValue().isEmpty())
  248. {
  249. const QString newFolderName = inputDlg.textValue();
  250. const QString newFolderPath = parentFullPath + "/" + newFolderName;
  251. if (!AZ::StringFunc::Path::IsValid(newFolderName.toUtf8().data()))
  252. {
  253. QMessageBox box(this);
  254. box.setText(tr("Please enter a single, valid folder name(standard English alphanumeric characters only)"));
  255. box.setIcon(QMessageBox::Critical);
  256. box.exec();
  257. return;
  258. }
  259. if (CFileUtil::PathExists(newFolderPath))
  260. {
  261. QMessageBox box(this);
  262. box.setText(tr("Folder already exists"));
  263. box.setIcon(QMessageBox::Critical);
  264. box.exec();
  265. return;
  266. }
  267. // The trailing / is important, otherwise CreatePath doesn't work
  268. if (!CFileUtil::CreatePath(newFolderPath + "/"))
  269. {
  270. QMessageBox box(this);
  271. box.setText(tr("Could not create folder"));
  272. box.setIcon(QMessageBox::Critical);
  273. box.exec();
  274. return;
  275. }
  276. m_model->AddItem(newFolderName, m_filterModel->mapToSource(index));
  277. ui->treeView->expand(index);
  278. }
  279. }
  280. else
  281. {
  282. QMessageBox box(this);
  283. box.setText(tr("Please select a folder first"));
  284. box.setIcon(QMessageBox::Critical);
  285. box.exec();
  286. return;
  287. }
  288. }
  289. void CLevelFileDialog::OnFilterChanged()
  290. {
  291. m_filterModel->setFilterText(ui->filterLineEdit->text().toLower());
  292. }
  293. void CLevelFileDialog::OnNameChanged()
  294. {
  295. if (!m_bOpenDialog)
  296. {
  297. QString errorMessage;
  298. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ValidateSaveLevelPath(errorMessage));
  299. }
  300. }
  301. void CLevelFileDialog::ReloadTree()
  302. {
  303. m_model->ReloadTree(m_bOpenDialog);
  304. }
  305. //////////////////////////////////////////////////////////////////////////
  306. // Heuristic to detect a level folder, also returns all .cry/.ly files in it
  307. //////////////////////////////////////////////////////////////////////////
  308. bool CLevelFileDialog::CheckLevelFolder(const QString folder, QStringList* levelFiles)
  309. {
  310. CFileEnum fileEnum;
  311. QFileInfo fileData;
  312. bool bIsLevelFolder = false;
  313. for (bool bFoundFile = fileEnum.StartEnumeration(folder, "*", &fileData);
  314. bFoundFile; bFoundFile = fileEnum.GetNextFile(&fileData))
  315. {
  316. const QString fileName = fileData.fileName();
  317. if (!fileData.isDir())
  318. {
  319. QString ext = "." + Path::GetExt(fileName);
  320. const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
  321. if (ext == defaultExtension)
  322. {
  323. bIsLevelFolder = true;
  324. if (levelFiles)
  325. {
  326. levelFiles->push_back(fileName);
  327. }
  328. }
  329. }
  330. }
  331. return bIsLevelFolder;
  332. }
  333. bool CLevelFileDialog::ValidateSaveLevelPath(QString& errorMessage) const
  334. {
  335. const QString enteredPath = GetEnteredPath();
  336. const QString levelPath = GetLevelPath();
  337. if (!AZ::StringFunc::Path::IsValid(Path::GetFileName(levelPath).toUtf8().data()))
  338. {
  339. errorMessage = tr("Please enter a valid level name (standard English alphanumeric characters only)");
  340. return false;
  341. }
  342. //Verify that we are not using the temporary level name
  343. const char* temporaryLevelName = GetIEditor()->GetDocument()->GetTemporaryLevelName();
  344. if (QString::compare(Path::GetFileName(levelPath), temporaryLevelName) == 0)
  345. {
  346. errorMessage = tr("Please enter a level name that is different from the temporary name");
  347. return false;
  348. }
  349. if (!ValidateLevelPath(enteredPath))
  350. {
  351. errorMessage = tr("Please enter a valid level location.\nYou cannot save levels inside levels.");
  352. return false;
  353. }
  354. if (CFileUtil::FileExists(levelPath))
  355. {
  356. errorMessage = tr("A file with that name already exists");
  357. return false;
  358. }
  359. if (CFileUtil::PathExists(levelPath) && !CheckLevelFolder(levelPath))
  360. {
  361. errorMessage = tr("Please enter a level name");
  362. return false;
  363. }
  364. if (!ui->nameLineEdit->hasAcceptableInput())
  365. {
  366. QString message = tr("The level name %1 contains illegal characters.");
  367. errorMessage = message.arg(enteredPath);
  368. return false;
  369. }
  370. return true;
  371. }
  372. //////////////////////////////////////////////////////////////////////////
  373. // Checks if a given path is a valid level path
  374. //////////////////////////////////////////////////////////////////////////
  375. bool CLevelFileDialog::ValidateLevelPath(const QString& levelPath) const
  376. {
  377. if (levelPath.isEmpty() || Path::GetExt(levelPath) != "")
  378. {
  379. return false;
  380. }
  381. // Split path
  382. QStringList splittedPath = levelPath.split(QRegularExpression(QStringLiteral(R"([\\/])")), Qt::SkipEmptyParts);
  383. // This shouldn't happen, but be careful
  384. if (splittedPath.empty())
  385. {
  386. return false;
  387. }
  388. // Make sure that no folder before the last in the name contains a level
  389. if (splittedPath.size() > 1)
  390. {
  391. QString currentPath = (Path::GetEditingGameDataFolder() + "/" + kLevelsFolder).c_str();
  392. for (size_t i = 0; i < splittedPath.size() - 1; ++i)
  393. {
  394. currentPath += "/" + splittedPath[static_cast<int>(i)];
  395. if (CFileUtil::FileExists(currentPath) || CheckLevelFolder(currentPath))
  396. {
  397. return false;
  398. }
  399. }
  400. }
  401. return true;
  402. }
  403. void CLevelFileDialog::SaveLastUsedLevelPath()
  404. {
  405. const QString settingPath = QString(Path::GetUserSandboxFolder()) + lastLoadPathFilename;
  406. XmlNodeRef lastUsedLevelPathNode = XmlHelpers::CreateXmlNode("lastusedlevelpath");
  407. lastUsedLevelPathNode->setAttr("path", ui->nameLineEdit->text().toUtf8().data());
  408. lastUsedLevelPathNode->saveToFile(settingPath.toUtf8().data());
  409. }
  410. void CLevelFileDialog::LoadLastUsedLevelPath()
  411. {
  412. const QString settingPath = Path::GetUserSandboxFolder() + QString(lastLoadPathFilename);
  413. XmlNodeRef lastUsedLevelPathNode = XmlHelpers::LoadXmlFromFile(settingPath.toUtf8().data());
  414. if (lastUsedLevelPathNode == nullptr)
  415. {
  416. return;
  417. }
  418. QString lastLoadedFileName;
  419. lastUsedLevelPathNode->getAttr("path", lastLoadedFileName);
  420. if (m_filterModel->rowCount() < 1)
  421. {
  422. // Defensive, doesn't happen
  423. return;
  424. }
  425. QModelIndex currentIndex = m_filterModel->index(0, 0); // Start with "Levels/" node
  426. QStringList segments = Path::SplitIntoSegments(lastLoadedFileName);
  427. for (auto it = segments.cbegin(), end = segments.cend(); it != end; ++it)
  428. {
  429. const int numChildren = m_filterModel->rowCount(currentIndex);
  430. for (int i = 0; i < numChildren; ++i)
  431. {
  432. const QModelIndex subIndex = m_filterModel->index(i, 0, currentIndex);
  433. if (*it == subIndex.data(Qt::DisplayRole).toString())
  434. {
  435. ui->treeView->expand(currentIndex);
  436. currentIndex = subIndex;
  437. break;
  438. }
  439. }
  440. }
  441. if (currentIndex.isValid())
  442. {
  443. ui->treeView->selectionModel()->select(currentIndex, QItemSelectionModel::Select);
  444. }
  445. ui->nameLineEdit->setText(lastLoadedFileName);
  446. }
  447. #include <moc_LevelFileDialog.cpp>