You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Sandbox/Editor/LevelFileDialog.cpp

550 lines
16 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
#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>