2
0

NewLevelDialog.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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 <AzCore/Utils/Utils.h>
  10. #include <AzCore/Settings/SettingsRegistryVisitorUtils.h>
  11. #include "NewLevelDialog.h"
  12. // Qt
  13. #include <QtWidgets/QPushButton>
  14. #include <QFileDialog>
  15. #include <QMessageBox>
  16. #include <QTimer>
  17. #include <QToolButton>
  18. #include <QListWidgetItem>
  19. #include <ui_NewLevelDialog.h>
  20. // Folder in which levels are stored
  21. static const char kNewLevelDialog_LevelsFolder[] = "Levels";
  22. static constexpr const char* RegistryKey_CustomTemplatePaths = "/O3DE/Preferences/Prefab/CustomTemplatePaths";
  23. static constexpr const char* DefaultTemplate = "Default_Level.prefab";
  24. class LevelFolderValidator : public QValidator
  25. {
  26. public:
  27. LevelFolderValidator(QObject* parent)
  28. : QValidator(parent)
  29. {
  30. m_parentDialog = qobject_cast<CNewLevelDialog*>(parent);
  31. }
  32. QValidator::State validate([[maybe_unused]] QString& input, [[maybe_unused]] int& pos) const override
  33. {
  34. if (m_parentDialog->ValidateLevel())
  35. {
  36. return QValidator::Acceptable;
  37. }
  38. return QValidator::Intermediate;
  39. }
  40. private:
  41. CNewLevelDialog* m_parentDialog;
  42. };
  43. static QString ChangeFileExtension(const QString& filePath, const QString& newExtension)
  44. {
  45. QFileInfo fileInfo(filePath);
  46. QString newFilePath = fileInfo.absolutePath() + QDir::separator() + fileInfo.baseName() + "." + newExtension;
  47. return newFilePath;
  48. }
  49. // CNewLevelDialog dialog
  50. CNewLevelDialog::CNewLevelDialog(QWidget* pParent /*=nullptr*/)
  51. : QDialog(pParent)
  52. , ui(new Ui::CNewLevelDialog)
  53. , m_initialized(false)
  54. {
  55. ui->setupUi(this);
  56. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  57. setWindowTitle(tr("New Level"));
  58. setStyleSheet("QListWidget::item {height: 148px; padding-left: 0px; padding-right: 0px; background-color: transparent;}");
  59. InitTemplateListWidget();
  60. // Level name only supports ASCII characters
  61. QRegExp rx("[_a-zA-Z0-9-]+");
  62. QValidator* validator = new QRegExpValidator(rx, this);
  63. ui->LEVEL->setValidator(validator);
  64. validator = new LevelFolderValidator(this);
  65. ui->LEVEL_FOLDERS->lineEdit()->setValidator(validator);
  66. ui->LEVEL_FOLDERS->setErrorToolTip(
  67. QString("The location must be a folder underneath the current project's %1 folder. (%2)")
  68. .arg(kNewLevelDialog_LevelsFolder)
  69. .arg(GetLevelsFolder()));
  70. ui->LEVEL_FOLDERS->setClearButtonEnabled(true);
  71. QToolButton* clearButton = AzQtComponents::LineEdit::getClearButton(ui->LEVEL_FOLDERS->lineEdit());
  72. assert(clearButton);
  73. connect(clearButton, &QToolButton::clicked, this, &CNewLevelDialog::OnClearButtonClicked);
  74. connect(ui->LEVEL_FOLDERS->lineEdit(), &QLineEdit::textEdited, this, &CNewLevelDialog::OnLevelNameChange);
  75. connect(ui->LEVEL_FOLDERS, &AzQtComponents::BrowseEdit::attachedButtonTriggered, this, &CNewLevelDialog::PopupAssetPicker);
  76. connect(ui->LEVEL, &QLineEdit::textChanged, this, &CNewLevelDialog::OnLevelNameChange);
  77. m_levelFolders = GetLevelsFolder();
  78. m_level = "";
  79. // First of all, keyboard focus is related to widget tab order, and the default tab order is based on the order in which
  80. // widgets are constructed. Therefore, creating more widgets changes the keyboard focus. That is why setFocus() is called last.
  81. // in OnStartup()
  82. // Secondly, using singleShot() allows OnStartup() slot of the QLineEdit instance to be invoked right after the event system
  83. // is ready to do so. Therefore, it is better to use singleShot() than directly call OnStartup().
  84. QTimer::singleShot(0, this, &CNewLevelDialog::OnStartup);
  85. ReloadLevelFolder();
  86. }
  87. CNewLevelDialog::~CNewLevelDialog()
  88. {
  89. }
  90. void CNewLevelDialog::InitTemplateListWidget() const
  91. {
  92. ui->listTemplates->clear();
  93. QStringList templatePaths;
  94. if (AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry)
  95. {
  96. auto AppendCustomTemplatePath = [&templatePaths](const AZ::SettingsRegistryInterface::VisitArgs& visitArgs)
  97. {
  98. AZ::IO::FixedMaxPath customTemplatePath;
  99. if (visitArgs.m_registry.Get(customTemplatePath.Native(), visitArgs.m_jsonKeyPath))
  100. {
  101. if (AZ::IO::FileIOBase::GetInstance()->ResolvePath(customTemplatePath, customTemplatePath))
  102. {
  103. templatePaths.push_back(QString::fromUtf8(customTemplatePath.c_str(), int(customTemplatePath.Native().size())));
  104. }
  105. }
  106. return AZ::SettingsRegistryInterface::VisitResponse::Skip;
  107. };
  108. AZ::SettingsRegistryVisitorUtils::VisitObject(*settingsRegistry, AppendCustomTemplatePath, RegistryKey_CustomTemplatePaths);
  109. }
  110. // Get all prefab files.
  111. const QStringList fileFilter = {"*.prefab"};
  112. QStringList allTemplateFiles;
  113. int defaultItem = 0;
  114. for(const QString& path: templatePaths)
  115. {
  116. QDir projectTemplateDirectory(path);
  117. projectTemplateDirectory.setNameFilters(fileFilter);
  118. const QStringList projectTemplateFiles = projectTemplateDirectory.entryList(QDir::Files);
  119. for (const QString& fileName: projectTemplateFiles)
  120. {
  121. if (fileName.compare(QString::fromUtf8(DefaultTemplate), Qt::CaseInsensitive) == 0)
  122. {
  123. defaultItem = allTemplateFiles.size();
  124. }
  125. allTemplateFiles.push_back(projectTemplateDirectory.filePath(fileName));
  126. }
  127. }
  128. // Create the item with its icons to the QListWidget.
  129. const QIcon defaultIcon(":/NewLevel/res/Prefab_80.svg");
  130. for (const QString& fileName: allTemplateFiles)
  131. {
  132. QFileInfo info(fileName);
  133. auto* item = new QListWidgetItem(info.baseName());
  134. const QString iconPath = ChangeFileExtension(fileName, "png");
  135. const QIcon itemIcon = QFile::exists(iconPath) ? QIcon(iconPath) : defaultIcon;
  136. item->setIcon(itemIcon);
  137. item->setData(Qt::UserRole, fileName);
  138. ui->listTemplates->addItem(item);
  139. }
  140. const QSize iconSize(128, 128);
  141. ui->listTemplates->setViewMode(QListWidget::IconMode);
  142. ui->listTemplates->setIconSize(iconSize);
  143. ui->listTemplates->setDragDropMode(QAbstractItemView::NoDragDrop);
  144. if (ui->listTemplates->count() > 0)
  145. {
  146. ui->listTemplates->setCurrentRow(defaultItem);
  147. }
  148. }
  149. QString CNewLevelDialog::GetTemplateName() const
  150. {
  151. const auto* item = ui->listTemplates->currentItem();
  152. if (item == nullptr)
  153. {
  154. if (ui->listTemplates->count() > 0)
  155. {
  156. // for safety, return the 0th item.
  157. return ui->listTemplates->item(0)->data(Qt::UserRole).toString();
  158. }
  159. else
  160. {
  161. // if we have no templates at all, return an empty string.
  162. return QString();
  163. }
  164. }
  165. const QString name =item->data(Qt::UserRole).toString();
  166. return name;
  167. }
  168. void CNewLevelDialog::OnStartup()
  169. {
  170. UpdateData(false);
  171. }
  172. void CNewLevelDialog::UpdateData(bool fromUi)
  173. {
  174. if (fromUi)
  175. {
  176. m_level = ui->LEVEL->text();
  177. m_levelFolders = ui->LEVEL_FOLDERS->text();
  178. }
  179. else
  180. {
  181. ui->LEVEL->setText(m_level);
  182. ui->LEVEL_FOLDERS->lineEdit()->setText(m_levelFolders);
  183. }
  184. }
  185. // CNewLevelDialog message handlers
  186. void CNewLevelDialog::OnInitDialog()
  187. {
  188. ReloadLevelFolder();
  189. // Disable OK until some text is entered
  190. if (QPushButton* button = ui->buttonBox->button(QDialogButtonBox::Ok))
  191. {
  192. button->setEnabled(false);
  193. }
  194. // Save data.
  195. UpdateData(false);
  196. }
  197. //////////////////////////////////////////////////////////////////////////
  198. void CNewLevelDialog::ReloadLevelFolder()
  199. {
  200. ui->LEVEL_FOLDERS->lineEdit()->clear();
  201. ui->LEVEL_FOLDERS->setText(QString(kNewLevelDialog_LevelsFolder) + '/');
  202. }
  203. QString CNewLevelDialog::GetLevelsFolder() const
  204. {
  205. QDir projectDir = QDir(Path::GetEditingGameDataFolder().c_str());
  206. QDir projectLevelsDir = QDir(QStringLiteral("%1/%2").arg(projectDir.absolutePath()).arg(kNewLevelDialog_LevelsFolder));
  207. return projectLevelsDir.absolutePath();
  208. }
  209. //////////////////////////////////////////////////////////////////////////
  210. QString CNewLevelDialog::GetLevel() const
  211. {
  212. QString output = m_level;
  213. QDir projectLevelsDir = QDir(GetLevelsFolder());
  214. if (!m_levelFolders.isEmpty())
  215. {
  216. output = m_levelFolders + "/" + m_level;
  217. }
  218. QString relativePath = projectLevelsDir.relativeFilePath(output);
  219. return relativePath;
  220. }
  221. bool CNewLevelDialog::ValidateLevel()
  222. {
  223. // Check that the selected folder is in or below the project/LEVELS folder.
  224. QDir projectLevelsDir = QDir(GetLevelsFolder());
  225. QString selectedFolder = ui->LEVEL_FOLDERS->text();
  226. QString absolutePath = QDir::cleanPath(projectLevelsDir.absoluteFilePath(selectedFolder));
  227. QString relativePath = projectLevelsDir.relativeFilePath(absolutePath);
  228. // Prevent saving to a different drive.
  229. if (projectLevelsDir.absolutePath()[0] != absolutePath[0])
  230. {
  231. return false;
  232. }
  233. if (relativePath.startsWith(".."))
  234. {
  235. return false;
  236. }
  237. return true;
  238. }
  239. void CNewLevelDialog::OnLevelNameChange()
  240. {
  241. UpdateData(true);
  242. // QRegExpValidator means the string will always be valid as long as it's not empty:
  243. bool valid = !m_level.isEmpty() && ValidateLevel();
  244. if (valid)
  245. {
  246. QDir levelDir(QString("%1/%2/").arg(m_levelFolders, m_level));
  247. QString strLevelPath = levelDir.absoluteFilePath(m_level + EditorUtils::LevelFile::GetDefaultFileExtension());
  248. if (strLevelPath.length() >= AZ::IO::MaxPathLength)
  249. {
  250. valid = false;
  251. int levelMaxLength = (AZ::IO::MaxPathLength - m_levelFolders.length() - QString(EditorUtils::LevelFile::GetDefaultFileExtension()).length() - 2) / 2;
  252. QMessageBox::warning(this, tr("Unable to Save Level"), QObject::tr("The level name is too long, the maximum is '%1'.").arg(levelMaxLength), QMessageBox::Ok, QMessageBox::Ok);
  253. }
  254. }
  255. // Use the validity to dynamically change the Ok button's enabled state
  256. if (QPushButton* button = ui->buttonBox->button(QDialogButtonBox::Ok))
  257. {
  258. button->setEnabled(valid);
  259. }
  260. }
  261. void CNewLevelDialog::OnClearButtonClicked()
  262. {
  263. ui->LEVEL_FOLDERS->lineEdit()->setText(GetLevelsFolder());
  264. UpdateData(true);
  265. }
  266. void CNewLevelDialog::PopupAssetPicker()
  267. {
  268. QString newPath = QFileDialog::getExistingDirectory(nullptr, QObject::tr("Choose Destination Folder"), GetLevelsFolder());
  269. if (!newPath.isEmpty())
  270. {
  271. ui->LEVEL_FOLDERS->setText(newPath);
  272. OnLevelNameChange();
  273. }
  274. }
  275. //////////////////////////////////////////////////////////////////////////
  276. void CNewLevelDialog::showEvent(QShowEvent* event)
  277. {
  278. if (!m_initialized)
  279. {
  280. OnInitDialog();
  281. m_initialized = true;
  282. }
  283. QDialog::showEvent(event);
  284. }
  285. #include <moc_NewLevelDialog.cpp>