LevelFileDialog.cpp 16 KB

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