NewLevelDialog.cpp 10 KB

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