git mv Code\Sandbox\Editor Code/Editor
Signed-off-by: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
#include "EditorDefs.h"
|
||||
|
||||
#include "LevelFileDialog.h"
|
||||
|
||||
#include <AzFramework/API/ApplicationAPI.h>
|
||||
|
||||
// Qt
|
||||
#include <QMessageBox>
|
||||
#include <QInputDialog>
|
||||
|
||||
// Editor
|
||||
#include "LevelTreeModel.h"
|
||||
#include "CryEditDoc.h"
|
||||
#include "API/ToolsApplicationAPI.h"
|
||||
|
||||
|
||||
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
|
||||
#include <ui_LevelFileDialog.h>
|
||||
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
|
||||
|
||||
static const char lastLoadPathFilename[] = "lastLoadPath.preset";
|
||||
|
||||
// Folder in which levels are stored
|
||||
static const char kLevelsFolder[] = "Levels";
|
||||
|
||||
// List of folder names that are used to detect a level folder
|
||||
static const char* kLevelFolderNames[] =
|
||||
{
|
||||
"Layers",
|
||||
"Minimap",
|
||||
"LevelData"
|
||||
};
|
||||
|
||||
// List of files that are used to detect a level folder
|
||||
static const char* kLevelFileNames[] =
|
||||
{
|
||||
"level.pak",
|
||||
"filelist.xml",
|
||||
"levelshadercache.pak",
|
||||
};
|
||||
|
||||
CLevelFileDialog::CLevelFileDialog(bool openDialog, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_bOpenDialog(openDialog)
|
||||
, ui(new Ui::LevelFileDialog())
|
||||
, m_model(new LevelTreeModel(this))
|
||||
, m_filterModel(new LevelTreeModelFilter(this))
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->treeView->header()->close();
|
||||
m_filterModel->setSourceModel(m_model);
|
||||
ui->treeView->setModel(m_filterModel);
|
||||
ui->treeView->installEventFilter(this);
|
||||
|
||||
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
this, &CLevelFileDialog::OnTreeSelectionChanged);
|
||||
|
||||
connect(ui->treeView, &QTreeView::doubleClicked, this, [this]()
|
||||
{
|
||||
if (m_bOpenDialog && !IsValidLevelSelected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnOK();
|
||||
});
|
||||
|
||||
connect(ui->filterLineEdit, &QLineEdit::textChanged, this, &CLevelFileDialog::OnFilterChanged);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CLevelFileDialog::OnCancel);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &CLevelFileDialog::OnOK);
|
||||
connect(ui->newFolderButton, &QPushButton::clicked, this, &CLevelFileDialog::OnNewFolder);
|
||||
|
||||
if (m_bOpenDialog)
|
||||
{
|
||||
setWindowTitle(tr("Open Level"));
|
||||
ui->newFolderButton->setVisible(false);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Open"));
|
||||
}
|
||||
else
|
||||
{
|
||||
setWindowTitle(tr("Save Level As "));
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Save"));
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
|
||||
// Make the name input the default active field for the save as dialog
|
||||
// The filter input will still be the default active field for the open dialog
|
||||
setTabOrder(ui->nameLineEdit, ui->filterLineEdit);
|
||||
|
||||
connect(ui->nameLineEdit, &QLineEdit::textChanged, this, &CLevelFileDialog::OnNameChanged);
|
||||
}
|
||||
|
||||
// reject invalid file names (see CryStringUtils::IsValidFileName)
|
||||
ui->nameLineEdit->setValidator(new QRegExpValidator(QRegExp("^[a-zA-Z0-9_\\-./]*$"), ui->nameLineEdit));
|
||||
|
||||
ReloadTree();
|
||||
LoadLastUsedLevelPath();
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
}
|
||||
|
||||
CLevelFileDialog::~CLevelFileDialog()
|
||||
{
|
||||
}
|
||||
|
||||
QString CLevelFileDialog::GetFileName() const
|
||||
{
|
||||
return m_fileName;
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnCancel()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnOK()
|
||||
{
|
||||
if (m_bOpenDialog)
|
||||
{
|
||||
// For Open button
|
||||
if (!IsValidLevelSelected())
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Please enter a valid level name"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString errorMessage;
|
||||
if (!ValidateSaveLevelPath(errorMessage))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Error"), errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
QString levelPath = GetLevelPath();
|
||||
if (CFileUtil::PathExists(levelPath) && CheckLevelFolder(levelPath))
|
||||
{
|
||||
// there is already a level folder at that location, ask before for overwriting it
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Do you really want to overwrite '%1'?").arg(GetEnteredPath()));
|
||||
box.setIcon(QMessageBox::Warning);
|
||||
box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||
if (box.exec() != QMessageBox::Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_fileName = levelPath + "/" + Path::GetFileName(levelPath) + EditorUtils::LevelFile::GetDefaultFileExtension();
|
||||
}
|
||||
|
||||
SaveLastUsedLevelPath();
|
||||
accept();
|
||||
}
|
||||
|
||||
bool CLevelFileDialog::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto keyEvent = static_cast<QKeyEvent*>(event);
|
||||
if (keyEvent->key() == Qt::Key_Return) {
|
||||
OnOK();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
QString CLevelFileDialog::NameForIndex(const QModelIndex& index) const
|
||||
{
|
||||
QStringList tokens;
|
||||
QModelIndex idx = index;
|
||||
|
||||
while (idx.isValid() && idx.parent().isValid()) // the root one doesn't count
|
||||
{
|
||||
tokens.push_front(idx.data(Qt::DisplayRole).toString());
|
||||
idx = idx.parent();
|
||||
}
|
||||
|
||||
QString text = tokens.join('/');
|
||||
const bool isLevelFolder = index.data(LevelTreeModel::IsLevelFolderRole).toBool();
|
||||
if (!isLevelFolder && !text.isEmpty())
|
||||
{
|
||||
text += "/";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
bool CLevelFileDialog::IsValidLevelSelected()
|
||||
{
|
||||
QString levelPath = GetLevelPath();
|
||||
m_fileName = GetFileName(levelPath);
|
||||
|
||||
QString currentExtension = "." + Path::GetExt(m_fileName);
|
||||
|
||||
const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
|
||||
const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
|
||||
|
||||
bool isInvalidFileExtension = (currentExtension != defaultExtension && currentExtension != oldExtension);
|
||||
|
||||
if (!isInvalidFileExtension && CFileUtil::FileExists(m_fileName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString CLevelFileDialog::GetLevelPath() const
|
||||
{
|
||||
const QString enteredPath = GetEnteredPath();
|
||||
const QString levelPath = QString("%1/%2/%3").arg(Path::GetEditingGameDataFolder().c_str()).arg(kLevelsFolder).arg(enteredPath);
|
||||
return levelPath;
|
||||
}
|
||||
|
||||
QString CLevelFileDialog::GetEnteredPath() const
|
||||
{
|
||||
QString enteredPath = ui->nameLineEdit->text();
|
||||
enteredPath = enteredPath.trimmed();
|
||||
enteredPath = Path::RemoveBackslash(enteredPath);
|
||||
|
||||
return enteredPath;
|
||||
}
|
||||
|
||||
QString CLevelFileDialog::GetFileName(QString levelPath)
|
||||
{
|
||||
QStringList levelFiles;
|
||||
QString fileName;
|
||||
|
||||
if (CheckLevelFolder(levelPath, &levelFiles) && levelFiles.size() >= 1)
|
||||
{
|
||||
const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
|
||||
const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
|
||||
|
||||
// A level folder was entered. Prefer the .ly/.cry file with the
|
||||
// folder name, otherwise pick the first one in the list
|
||||
QString path = Path::GetFileName(levelPath);
|
||||
QString needle = path + defaultExtension;
|
||||
auto iter = std::find(levelFiles.begin(), levelFiles.end(), needle);
|
||||
|
||||
if (iter != levelFiles.end())
|
||||
{
|
||||
fileName = levelPath + "/" + *iter;
|
||||
}
|
||||
else
|
||||
{
|
||||
needle = path + oldExtension;
|
||||
iter = std::find(levelFiles.begin(), levelFiles.end(), needle);
|
||||
if (iter != levelFiles.end())
|
||||
{
|
||||
fileName = levelPath + "/" + *iter;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileName = levelPath + "/" + levelFiles[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise try to directly load the specified file (backward compatibility)
|
||||
fileName = levelPath;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnTreeSelectionChanged()
|
||||
{
|
||||
const QModelIndexList indexes = ui->treeView->selectionModel()->selectedIndexes();
|
||||
if (!indexes.isEmpty())
|
||||
{
|
||||
ui->nameLineEdit->setText(NameForIndex(indexes.first()));
|
||||
}
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnNewFolder()
|
||||
{
|
||||
const QModelIndexList indexes = ui->treeView->selectionModel()->selectedIndexes();
|
||||
|
||||
if (indexes.isEmpty())
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Please select a folder first"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex index = indexes.first();
|
||||
const bool isLevelFolder = index.data(LevelTreeModel::IsLevelFolderRole).toBool();
|
||||
|
||||
// Creating folders is not allowed in level folders
|
||||
if (!isLevelFolder && index.isValid())
|
||||
{
|
||||
const QString parentFullPath = index.data(LevelTreeModel::FullPathRole).toString();
|
||||
QInputDialog inputDlg(this);
|
||||
inputDlg.setLabelText(tr("Please select a folder name"));
|
||||
|
||||
if (inputDlg.exec() == QDialog::Accepted && !inputDlg.textValue().isEmpty())
|
||||
{
|
||||
const QString newFolderName = inputDlg.textValue();
|
||||
const QString newFolderPath = parentFullPath + "/" + newFolderName;
|
||||
|
||||
if (!CryStringUtils::IsValidFileName(newFolderName.toUtf8().data()))
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Please enter a single, valid folder name(standard English alphanumeric characters only)"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CFileUtil::PathExists(newFolderPath))
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Folder already exists"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
// The trailing / is important, otherwise CreatePath doesn't work
|
||||
if (!CFileUtil::CreatePath(newFolderPath + "/"))
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Could not create folder"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
m_model->AddItem(newFolderName, m_filterModel->mapToSource(index));
|
||||
ui->treeView->expand(index);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setText(tr("Please select a folder first"));
|
||||
box.setIcon(QMessageBox::Critical);
|
||||
box.exec();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnFilterChanged()
|
||||
{
|
||||
m_filterModel->setFilterText(ui->filterLineEdit->text().toLower());
|
||||
}
|
||||
|
||||
void CLevelFileDialog::OnNameChanged()
|
||||
{
|
||||
if (!m_bOpenDialog)
|
||||
{
|
||||
QString errorMessage;
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ValidateSaveLevelPath(errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void CLevelFileDialog::ReloadTree()
|
||||
{
|
||||
m_model->ReloadTree(m_bOpenDialog);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Heuristic to detect a level folder, also returns all .cry/.ly files in it
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
bool CLevelFileDialog::CheckLevelFolder(const QString folder, QStringList* levelFiles)
|
||||
{
|
||||
CFileEnum fileEnum;
|
||||
QFileInfo fileData;
|
||||
bool bIsLevelFolder = false;
|
||||
|
||||
for (bool bFoundFile = fileEnum.StartEnumeration(folder, "*", &fileData);
|
||||
bFoundFile; bFoundFile = fileEnum.GetNextFile(&fileData))
|
||||
{
|
||||
const QString fileName = fileData.fileName();
|
||||
|
||||
if (!fileData.isDir())
|
||||
{
|
||||
QString ext = "." + Path::GetExt(fileName);
|
||||
|
||||
const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
|
||||
|
||||
if (ext == defaultExtension)
|
||||
{
|
||||
bIsLevelFolder = true;
|
||||
|
||||
if (levelFiles)
|
||||
{
|
||||
levelFiles->push_back(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bIsLevelFolder;
|
||||
}
|
||||
|
||||
bool CLevelFileDialog::ValidateSaveLevelPath(QString& errorMessage) const
|
||||
{
|
||||
const QString enteredPath = GetEnteredPath();
|
||||
const QString levelPath = GetLevelPath();
|
||||
|
||||
if (!CryStringUtils::IsValidFileName(Path::GetFileName(levelPath).toUtf8().data()))
|
||||
{
|
||||
errorMessage = tr("Please enter a valid level name (standard English alphanumeric characters only)");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Verify that we are not using the temporary level name
|
||||
const char* temporaryLevelName = GetIEditor()->GetDocument()->GetTemporaryLevelName();
|
||||
if (QString::compare(Path::GetFileName(levelPath), temporaryLevelName) == 0)
|
||||
{
|
||||
errorMessage = tr("Please enter a level name that is different from the temporary name");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateLevelPath(enteredPath))
|
||||
{
|
||||
errorMessage = tr("Please enter a valid level location.\nYou cannot save levels inside levels.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CFileUtil::FileExists(levelPath))
|
||||
{
|
||||
errorMessage = tr("A file with that name already exists");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CFileUtil::PathExists(levelPath) && !CheckLevelFolder(levelPath))
|
||||
{
|
||||
errorMessage = tr("Please enter a level name");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Checks if a given path is a valid level path
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
bool CLevelFileDialog::ValidateLevelPath(const QString& levelPath) const
|
||||
{
|
||||
if (levelPath.isEmpty() || Path::GetExt(levelPath) != "")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split path
|
||||
QStringList splittedPath = levelPath.split(QRegularExpression(QStringLiteral(R"([\\/])")), Qt::SkipEmptyParts);
|
||||
|
||||
// This shouldn't happen, but be careful
|
||||
if (splittedPath.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure that no folder before the last in the name contains a level
|
||||
if (splittedPath.size() > 1)
|
||||
{
|
||||
QString currentPath = (Path::GetEditingGameDataFolder() + "/" + kLevelsFolder).c_str();
|
||||
for (size_t i = 0; i < splittedPath.size() - 1; ++i)
|
||||
{
|
||||
currentPath += "/" + splittedPath[i];
|
||||
|
||||
if (CFileUtil::FileExists(currentPath) || CheckLevelFolder(currentPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CLevelFileDialog::SaveLastUsedLevelPath()
|
||||
{
|
||||
const QString settingPath = QString(Path::GetUserSandboxFolder()) + lastLoadPathFilename;
|
||||
|
||||
XmlNodeRef lastUsedLevelPathNode = XmlHelpers::CreateXmlNode("lastusedlevelpath");
|
||||
lastUsedLevelPathNode->setAttr("path", ui->nameLineEdit->text().toUtf8().data());
|
||||
lastUsedLevelPathNode->saveToFile(settingPath.toUtf8().data());
|
||||
}
|
||||
|
||||
void CLevelFileDialog::LoadLastUsedLevelPath()
|
||||
{
|
||||
const QString settingPath = Path::GetUserSandboxFolder() + QString(lastLoadPathFilename);
|
||||
|
||||
XmlNodeRef lastUsedLevelPathNode = XmlHelpers::LoadXmlFromFile(settingPath.toUtf8().data());
|
||||
if (lastUsedLevelPathNode == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QString lastLoadedFileName;
|
||||
lastUsedLevelPathNode->getAttr("path", lastLoadedFileName);
|
||||
|
||||
if (m_filterModel->rowCount() < 1)
|
||||
{
|
||||
// Defensive, doesn't happen
|
||||
return;
|
||||
}
|
||||
|
||||
QModelIndex currentIndex = m_filterModel->index(0, 0); // Start with "Levels/" node
|
||||
QStringList segments = Path::SplitIntoSegments(lastLoadedFileName);
|
||||
for (auto it = segments.cbegin(), end = segments.cend(); it != end; ++it)
|
||||
{
|
||||
const int numChildren = m_filterModel->rowCount(currentIndex);
|
||||
for (int i = 0; i < numChildren; ++i)
|
||||
{
|
||||
const QModelIndex subIndex = m_filterModel->index(i, 0, currentIndex);
|
||||
if (*it == subIndex.data(Qt::DisplayRole).toString())
|
||||
{
|
||||
ui->treeView->expand(currentIndex);
|
||||
currentIndex = subIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIndex.isValid())
|
||||
{
|
||||
ui->treeView->selectionModel()->select(currentIndex, QItemSelectionModel::Select);
|
||||
}
|
||||
|
||||
ui->nameLineEdit->setText(lastLoadedFileName);
|
||||
}
|
||||
|
||||
#include <moc_LevelFileDialog.cpp>
|
||||
Reference in New Issue
Block a user