PrimeWorldEditor/src/Editor/ResourceBrowser/CResourceBrowser.cpp

1125 lines
36 KiB
C++

#include "CResourceBrowser.h"
#include "ui_CResourceBrowser.h"
#include "CProgressDialog.h"
#include "CResourceDelegate.h"
#include "CResourceTableContextMenu.h"
#include "Editor/CEditorApplication.h"
#include "Editor/Undo/CMoveDirectoryCommand.h"
#include "Editor/Undo/CMoveResourceCommand.h"
#include "Editor/Undo/CRenameDirectoryCommand.h"
#include "Editor/Undo/CRenameResourceCommand.h"
#include "Editor/Undo/CSaveStoreCommand.h"
#include "Editor/Undo/ICreateDeleteDirectoryCommand.h"
#include "Editor/Undo/ICreateDeleteResourceCommand.h"
#include <Core/GameProject/AssetNameGeneration.h>
#include <Core/GameProject/CAssetNameMap.h>
#include <QButtonGroup>
#include <QCheckBox>
#include <QFileDialog>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QtConcurrent/QtConcurrentRun>
CResourceBrowser::CResourceBrowser(QWidget *pParent)
: QWidget(pParent)
, mpUI(new Ui::CResourceBrowser)
, mpSelectedEntry(nullptr)
, mpStore(nullptr)
, mpSelectedDir(nullptr)
, mEditorStore(false)
, mAssetListMode(false)
, mSearching(false)
, mpAddMenu(nullptr)
, mpInspectedEntry(nullptr)
{
mpUI->setupUi(this);
setEnabled(false);
// Hide sorting combo box for now. The size isn't displayed on the UI so this isn't really useful for the end user.
mpUI->SortComboBox->hide();
// Create undo/redo actions
mpUndoAction = new QAction("Undo", this);
mpRedoAction = new QAction("Redo", this);
mpUndoAction->setShortcut( QKeySequence::Undo );
mpRedoAction->setShortcut( QKeySequence::Redo );
// todo - undo/redo commands are deactivated because they conflict with the World Editor undo/redo commands. fix this
#if 0
addAction(mpUndoAction);
addAction(mpRedoAction);
#endif
connect(mpUndoAction, SIGNAL(triggered(bool)), this, SLOT(Undo()));
connect(mpRedoAction, SIGNAL(triggered(bool)), this, SLOT(Redo()));
connect(&mUndoStack, SIGNAL(canUndoChanged(bool)), this, SLOT(UpdateUndoActionStates()));
connect(&mUndoStack, SIGNAL(canRedoChanged(bool)), this, SLOT(UpdateUndoActionStates()));
// Configure display mode buttons
QButtonGroup *pModeGroup = new QButtonGroup(this);
pModeGroup->addButton(mpUI->ResourceTreeButton);
pModeGroup->addButton(mpUI->ResourceListButton);
pModeGroup->setExclusive(true);
// Set up table models
mpModel = new CResourceTableModel(this, this);
mpProxyModel = new CResourceProxyModel(this);
mpProxyModel->setSourceModel(mpModel);
mpUI->ResourceTableView->setModel(mpProxyModel);
mpUI->ResourceTableView->resizeRowsToContents();
QHeaderView *pHeader = mpUI->ResourceTableView->horizontalHeader();
pHeader->setSectionResizeMode(0, QHeaderView::Stretch);
mpDelegate = new CResourceBrowserDelegate(this);
mpUI->ResourceTableView->setItemDelegate(mpDelegate);
mpUI->ResourceTableView->installEventFilter(this);
// Set up directory tree model
mpDirectoryModel = new CVirtualDirectoryModel(this, this);
mpUI->DirectoryTreeView->setModel(mpDirectoryModel);
RefreshResources();
UpdateDescriptionLabel();
// Set up filter checkboxes
mpFilterBoxesLayout = new QVBoxLayout();
mpFilterBoxesLayout->setContentsMargins(2, 2, 2, 2);
mpFilterBoxesLayout->setSpacing(1);
mpFilterBoxesContainerWidget = new QWidget(this);
mpFilterBoxesContainerWidget->setLayout(mpFilterBoxesLayout);
mpUI->FilterCheckboxScrollArea->setWidget(mpFilterBoxesContainerWidget);
mpUI->FilterCheckboxScrollArea->setBackgroundRole(QPalette::AlternateBase);
mFilterBoxFont = font();
mFilterBoxFont.setPointSize(mFilterBoxFont.pointSize() - 1);
QFont AllBoxFont = mFilterBoxFont;
AllBoxFont.setBold(true);
mpFilterAllBox = new QCheckBox(this);
mpFilterAllBox->setChecked(true);
mpFilterAllBox->setFont(AllBoxFont);
mpFilterAllBox->setText("All");
mpFilterBoxesLayout->addWidget(mpFilterAllBox);
CreateFilterCheckboxes();
// Set up the options menu
QMenu *pOptionsMenu = new QMenu(this);
QMenu *pImportMenu = pOptionsMenu->addMenu("Import Names");
pOptionsMenu->addAction("Export Names", this, SLOT(ExportAssetNames()));
pOptionsMenu->addSeparator();
pImportMenu->addAction("Asset Name Map", this, SLOT(ImportAssetNameMap()));
pImportMenu->addAction("Package Contents List", this, SLOT(ImportPackageContentsList()));
pImportMenu->addAction("Generate Asset Names", this, SLOT(GenerateAssetNames()));
QAction *pDisplayAssetIDsAction = new QAction("Display Asset IDs", this);
pDisplayAssetIDsAction->setCheckable(true);
connect(pDisplayAssetIDsAction, SIGNAL(toggled(bool)), this, SLOT(SetAssetIDDisplayEnabled(bool)));
pOptionsMenu->addAction(pDisplayAssetIDsAction);
pOptionsMenu->addAction("Find Asset by ID", this, SLOT(FindAssetByID()));
pOptionsMenu->addAction("Rebuild Database", this, SLOT(RebuildResourceDB()));
mpUI->OptionsToolButton->setMenu(pOptionsMenu);
#if !PUBLIC_RELEASE
// Only add the store menu in debug builds. We don't want end users editing the editor store.
pOptionsMenu->addSeparator();
QMenu *pStoreMenu = pOptionsMenu->addMenu("Set Store");
QAction *pProjStoreAction = pStoreMenu->addAction("Project Store", this, SLOT(SetProjectStore()));
QAction *pEdStoreAction = pStoreMenu->addAction("Editor Store", this, SLOT(SetEditorStore()));
pProjStoreAction->setCheckable(true);
pProjStoreAction->setChecked(true);
pEdStoreAction->setCheckable(true);
QActionGroup *pGroup = new QActionGroup(this);
pGroup->addAction(pProjStoreAction);
pGroup->addAction(pEdStoreAction);
#endif
// Resize splitter
mpUI->splitter->setSizes( QList<int>() << width() * 0.4 << width() * 0.6 );
// Create context menu for the resource table
new CResourceTableContextMenu(this, mpUI->ResourceTableView, mpModel, mpProxyModel);
// Set up connections
connect(mpUI->SearchBar, SIGNAL(StoppedTyping(QString)), this, SLOT(OnSearchStringChanged(QString)));
connect(mpUI->ResourceTreeButton, SIGNAL(pressed()), this, SLOT(SetResourceTreeView()));
connect(mpUI->ResourceListButton, SIGNAL(pressed()), this, SLOT(SetResourceListView()));
connect(mpUI->SortComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(OnSortModeChanged(int)));
connect(mpUI->ClearButton, SIGNAL(pressed()), this, SLOT(OnClearButtonPressed()));
connect(mpUI->DirectoryTreeView, SIGNAL(clicked(QModelIndex)), this, SLOT(OnDirectorySelectionChanged(QModelIndex)));
connect(mpUI->DirectoryTreeView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(OnDirectorySelectionChanged(QModelIndex)));
connect(mpUI->ResourceTableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(OnDoubleClickTable(QModelIndex)));
connect(mpUI->ResourceTableView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(OnResourceSelectionChanged(QModelIndex)));
connect(mpProxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), mpUI->ResourceTableView, SLOT(resizeRowsToContents()));
connect(mpProxyModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)), mpUI->ResourceTableView, SLOT(resizeRowsToContents()));
connect(mpProxyModel, SIGNAL(modelReset()), mpUI->ResourceTableView, SLOT(resizeRowsToContents()));
connect(mpFilterAllBox, SIGNAL(toggled(bool)), this, SLOT(OnFilterTypeBoxTicked(bool)));
connect(gpEdApp, SIGNAL(ActiveProjectChanged(CGameProject*)), this, SLOT(UpdateStore()));
}
CResourceBrowser::~CResourceBrowser()
{
delete mpUI;
}
void CResourceBrowser::SetActiveDirectory(CVirtualDirectory *pDir)
{
if (mpSelectedDir != pDir || mpModel->IsDisplayingUserEntryList())
{
mpSelectedDir = pDir;
RefreshResources();
UpdateDescriptionLabel();
if (sender() != mpUI->DirectoryTreeView)
{
QModelIndex Index = mpDirectoryModel->GetIndexForDirectory(pDir);
mpUI->DirectoryTreeView->selectionModel()->setCurrentIndex(Index, QItemSelectionModel::ClearAndSelect);
}
}
}
void CResourceBrowser::SelectResource(CResourceEntry *pEntry, bool ClearFiltersIfNecessary /*= false*/)
{
ASSERT(pEntry);
// Set directory active
SetActiveDirectory(pEntry->Directory());
// Clear search
if (!mpUI->SearchBar->text().isEmpty())
{
mpUI->SearchBar->clear();
UpdateFilter();
}
// Change filter
if (ClearFiltersIfNecessary && !mpProxyModel->IsTypeAccepted(pEntry->TypeInfo()))
{
ResetTypeFilter();
}
// Select resource
QModelIndex SourceIndex = mpModel->GetIndexForEntry(pEntry);
QModelIndex ProxyIndex = mpProxyModel->mapFromSource(SourceIndex);
if (ProxyIndex.isValid())
{
// Note: We have to call scrollToBottom() first or else sometimes scrollTo() doesn't work at all
// in large folders (like Uncategorized). Dumb but the only solution I've been able to figure out.
mpUI->ResourceTableView->selectionModel()->select(ProxyIndex, QItemSelectionModel::ClearAndSelect);
mpUI->ResourceTableView->scrollToBottom();
mpUI->ResourceTableView->scrollTo(ProxyIndex, QAbstractItemView::PositionAtCenter);
}
}
void CResourceBrowser::SelectDirectory(CVirtualDirectory *pDir)
{
ASSERT(pDir);
ASSERT(!pDir->IsRoot());
// Set parent directory active
SetActiveDirectory(pDir->Parent());
// Clear search
if (!mpUI->SearchBar->text().isEmpty())
{
mpUI->SearchBar->clear();
UpdateFilter();
}
// Select directory
QModelIndex SourceIndex = mpModel->GetIndexForDirectory(pDir);
QModelIndex ProxyIndex = mpProxyModel->mapFromSource(SourceIndex);
mpUI->ResourceTableView->selectionModel()->select(ProxyIndex, QItemSelectionModel::ClearAndSelect);
mpUI->ResourceTableView->scrollTo(ProxyIndex, QAbstractItemView::PositionAtCenter);
}
void CResourceBrowser::CreateFilterCheckboxes()
{
// Delete existing checkboxes
foreach (const SResourceType& rkType, mTypeList)
delete rkType.pFilterCheckBox;
mTypeList.clear();
if (mpStore)
{
// Get new type list
std::list<CResTypeInfo*> TypeList;
CResTypeInfo::GetAllTypesInGame(mpStore->Game(), TypeList);
for (auto Iter = TypeList.begin(); Iter != TypeList.end(); Iter++)
{
CResTypeInfo *pType = *Iter;
QCheckBox *pCheck = new QCheckBox(this);
pCheck->setFont(mFilterBoxFont);
pCheck->setText(TO_QSTRING(pType->TypeName()));
mTypeList << SResourceType { pType, pCheck };
}
std::sort(mTypeList.begin(), mTypeList.end(), [](const SResourceType& rkLeft, const SResourceType& rkRight) -> bool {
return rkLeft.pTypeInfo->TypeName().ToUpper() < rkRight.pTypeInfo->TypeName().ToUpper();
});
// Add sorted checkboxes to the UI
foreach (const SResourceType& rkType, mTypeList)
{
QCheckBox *pCheck = rkType.pFilterCheckBox;
mpFilterBoxesLayout->addWidget(rkType.pFilterCheckBox);
connect(pCheck, SIGNAL(toggled(bool)), this, SLOT(OnFilterTypeBoxTicked(bool)));
}
}
QSpacerItem *pSpacer = new QSpacerItem(0, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
mpFilterBoxesLayout->addSpacerItem(pSpacer);
}
void CResourceBrowser::CreateAddMenu()
{
// Delete any existing menu
if (mpAddMenu)
{
delete mpAddMenu;
mpAddMenu = nullptr;
}
// Create new one, only if we have a valid resource store.
if (mpStore)
{
mpAddMenu = new QMenu(this);
mpAddMenu->addAction("New Folder", this, SLOT(CreateDirectory()));
mpAddMenu->addSeparator();
QMenu* pCreateMenu = new QMenu("Create...", mpAddMenu);
mpAddMenu->addMenu(pCreateMenu);
AddCreateAssetMenuActions(pCreateMenu);
mpUI->AddButton->setMenu(mpAddMenu);
}
mpUI->AddButton->setEnabled( mpAddMenu != nullptr );
}
void CResourceBrowser::AddCreateAssetMenuActions(QMenu* pMenu)
{
std::list<CResTypeInfo*> TypeInfos;
CResTypeInfo::GetAllTypesInGame(mpStore->Game(), TypeInfos);
for (auto Iter = TypeInfos.begin(); Iter != TypeInfos.end(); Iter++)
{
CResTypeInfo* pTypeInfo = *Iter;
if (pTypeInfo->CanBeCreated())
{
QString TypeName = TO_QSTRING( pTypeInfo->TypeName() );
QAction* pAction = pMenu->addAction(TypeName, this, SLOT(OnCreateAssetAction()));
pAction->setProperty("TypeInfo", QVariant((int) pTypeInfo->Type()));
}
}
}
bool CResourceBrowser::RenameResource(CResourceEntry *pEntry, const TString& rkNewName)
{
if (pEntry->Name() == rkNewName)
return false;
// Check if the move is valid
if (!pEntry->CanMoveTo(pEntry->DirectoryPath(), rkNewName))
{
if (pEntry->Directory()->FindChildResource(rkNewName, pEntry->ResourceType()) != nullptr)
{
UICommon::ErrorMsg(this, "Failed to rename; the destination directory has conflicting files!");
return false;
}
else
{
UICommon::ErrorMsg(this, "Failed to rename; filename is invalid!");
return false;
}
}
// Everything seems to be valid; proceed with the rename
mUndoStack.beginMacro("Rename Resource");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CRenameResourceCommand(pEntry, rkNewName) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
bool CResourceBrowser::RenameDirectory(CVirtualDirectory *pDir, const TString& rkNewName)
{
if (pDir->Name() == rkNewName)
return false;
if (!CVirtualDirectory::IsValidDirectoryName(rkNewName))
{
UICommon::ErrorMsg(this, "Failed to rename; directory name is invalid!");
return false;
}
// Check for conflicts
if (pDir->Parent()->FindChildDirectory(rkNewName, false) != nullptr)
{
UICommon::ErrorMsg(this, "Failed to rename; the destination directory has a conflicting directory!");
return false;
}
// No conflicts, proceed with the rename
mUndoStack.beginMacro("Rename Directory");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CRenameDirectoryCommand(pDir, rkNewName) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
bool CResourceBrowser::MoveResources(const QList<CResourceEntry*>& rkResources, const QList<CVirtualDirectory*>& rkDirectories, CVirtualDirectory *pNewDir)
{
// Check for any conflicts
QList<CResourceEntry*> ConflictingResources;
QList<CResourceEntry*> ValidResources;
foreach (CResourceEntry *pEntry, rkResources)
{
CResourceEntry *pConflict = pNewDir->FindChildResource(pEntry->Name(), pEntry->ResourceType());
if (pConflict != pEntry)
{
if (pConflict != nullptr)
ConflictingResources << pEntry;
else
ValidResources << pEntry;
}
}
QList<CVirtualDirectory*> ConflictingDirs;
QList<CVirtualDirectory*> ValidDirs;
foreach (CVirtualDirectory *pDir, rkDirectories)
{
CVirtualDirectory *pConflict = pNewDir->FindChildDirectory(pDir->Name(), false);
if (pConflict != pDir)
{
if (pConflict != nullptr)
ConflictingDirs << pDir;
else
ValidDirs << pDir;
}
}
// If there were conflicts, notify the user of them
if (!ConflictingResources.isEmpty() || !ConflictingDirs.isEmpty())
{
QString ErrorMsg = "Failed to move; the destination directory has conflicting files.\n\n";
foreach (CVirtualDirectory *pDir, ConflictingDirs)
{
ErrorMsg += QString("* %1").arg( TO_QSTRING(pDir->Name()) );
}
foreach (CResourceEntry *pEntry, ConflictingResources)
{
ErrorMsg += QString("* %1.%2\n").arg( TO_QSTRING(pEntry->Name()) ).arg( TO_QSTRING(pEntry->CookedExtension().ToString()) );
}
UICommon::ErrorMsg(this, ErrorMsg);
return false;
}
// Create undo actions to actually perform the moves
if (!ValidResources.isEmpty() || !ValidDirs.isEmpty())
{
mUndoStack.beginMacro("Move Resources");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
foreach (CVirtualDirectory *pDir, ValidDirs)
mUndoStack.push( new CMoveDirectoryCommand(mpStore, pDir, pNewDir) );
foreach (CResourceEntry *pEntry, ValidResources)
mUndoStack.push( new CMoveResourceCommand(pEntry, pNewDir) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
}
return true;
}
CResourceEntry* CResourceBrowser::CreateNewResource(EResourceType Type,
TString Name /*= ""*/,
CVirtualDirectory* pDir /*= nullptr*/,
CAssetID ID /*= CAssetID()*/)
{
if (!pDir)
{
pDir = mpSelectedDir;
}
// Create new asset ID. Sanity check to make sure the ID is unused.
while (!ID.IsValid() || mpStore->FindEntry(ID) != nullptr)
{
ID = CAssetID::RandomID( mpStore->Game() );
}
// Boring generic default name - user will immediately be prompted to change this
TString BaseName = Name;
if (BaseName.IsEmpty())
{
BaseName = TString::Format(
"New %s", *CResTypeInfo::FindTypeInfo(Type)->TypeName()
);
}
Name = BaseName;
int Num = 0;
while (pDir->FindChildResource(Name, Type) != nullptr)
{
Num++;
Name = TString::Format("%s (%d)", *BaseName, Num);
}
// Create the actual resource
CResourceEntry* pEntry = mpStore->CreateNewResource(ID, Type, pDir->FullPath(), Name);
// Push undo command
mUndoStack.beginMacro("Create Resource");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.push( new CCreateResourceCommand(pEntry) );
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
pEntry->Save();
// Select new resource so user can enter a name
QModelIndex Index = mpModel->GetIndexForEntry(pEntry);
ASSERT(Index.isValid());
QModelIndex ProxyIndex = mpProxyModel->mapFromSource(Index);
mpUI->ResourceTableView->selectionModel()->select(ProxyIndex, QItemSelectionModel::ClearAndSelect);
mpUI->ResourceTableView->edit(ProxyIndex);
return pEntry;
}
bool CResourceBrowser::eventFilter(QObject *pWatched, QEvent *pEvent)
{
if (pWatched == mpUI->ResourceTableView)
{
if (pEvent->type() == QEvent::FocusIn || pEvent->type() == QEvent::FocusOut)
{
UpdateUndoActionStates();
}
}
return false;
}
void CResourceBrowser::RefreshResources()
{
// Fill resource table
mpModel->FillEntryList(mpSelectedDir, mAssetListMode || mSearching);
}
void CResourceBrowser::RefreshDirectories()
{
mpDirectoryModel->SetRoot(mpStore->RootDirectory());
// Clear selection. This function is called when directories are created/deleted and our current selection might not be valid anymore
QModelIndex RootIndex = mpDirectoryModel->index(0, 0, QModelIndex());
mpUI->DirectoryTreeView->selectionModel()->setCurrentIndex(RootIndex, QItemSelectionModel::ClearAndSelect);
mpUI->DirectoryTreeView->setExpanded(RootIndex, true);
}
void CResourceBrowser::UpdateDescriptionLabel()
{
// Update main description label
QString Desc;
if (mpStore)
{
QString ModelDesc = mpModel->ModelDescription();
if (mSearching)
{
QString SearchText = mpUI->SearchBar->text();
Desc = QString("Searching \"%1\" in: %2").arg(SearchText).arg(ModelDesc);
}
else
Desc = QString("Displaying: %1").arg(ModelDesc);
}
mpUI->TableDescriptionLabel->setText(Desc);
// Update clear button status
bool CanGoUp = (mpSelectedDir && !mpSelectedDir->IsRoot());
bool CanClear = (!mpUI->SearchBar->text().isEmpty() || mpModel->IsDisplayingUserEntryList());
mpUI->ClearButton->setEnabled(CanGoUp || CanClear);
mpUI->ClearButton->setIcon( CanClear ? QIcon(":/icons/X_16px.svg") : QIcon(":/icons/ToParentFolder_16px.svg") );
}
void CResourceBrowser::SetResourceTreeView()
{
mAssetListMode = false;
RefreshResources();
}
void CResourceBrowser::SetResourceListView()
{
mAssetListMode = true;
RefreshResources();
}
void CResourceBrowser::OnClearButtonPressed()
{
if (!mpUI->SearchBar->text().isEmpty())
{
ResetSearch();
}
else if (mpModel->IsDisplayingUserEntryList())
{
RefreshResources();
if (mpInspectedEntry)
{
SelectResource(mpInspectedEntry);
mpInspectedEntry = nullptr;
}
}
else
{
SelectDirectory(mpSelectedDir);
}
UpdateDescriptionLabel();
}
void CResourceBrowser::OnSortModeChanged(int Index)
{
CResourceProxyModel::ESortMode Mode = (Index == 0 ? CResourceProxyModel::ESortMode::ByName : CResourceProxyModel::ESortMode::BySize);
mpProxyModel->SetSortMode(Mode);
}
void CResourceBrowser::OnCreateAssetAction()
{
// Attempt to retrieve the asset type from the sender. If successful, create the asset.
QAction* pSender = qobject_cast<QAction*>(sender());
if (pSender)
{
bool Ok;
EResourceType Type = (EResourceType) pSender->property("TypeInfo").toInt(&Ok);
if (Ok)
{
CreateNewResource(Type);
}
}
}
bool CResourceBrowser::CreateDirectory()
{
if (mpSelectedDir)
{
TString DirNameBase = "New Folder";
TString DirName = DirNameBase;
uint32 AppendNum = 0;
while (mpSelectedDir->FindChildDirectory(DirName, false) != nullptr)
{
AppendNum++;
DirName = TString::Format("%s (%d)", *DirNameBase, AppendNum);
}
// Push create command to actually create the directory
mUndoStack.beginMacro("Create Directory");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
CCreateDirectoryCommand *pCmd = new CCreateDirectoryCommand(mpStore, mpSelectedDir->FullPath(), DirName);
mUndoStack.push(pCmd);
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
// Now fetch the new directory and start editing it so the user can enter a name
CVirtualDirectory *pNewDir = mpSelectedDir->FindChildDirectory(DirName, false);
if (!pNewDir) return false;
// todo: edit in the directory tree view instead if it has focus
if (!mpUI->DirectoryTreeView->hasFocus())
{
QModelIndex Index = mpModel->GetIndexForDirectory(pNewDir);
ASSERT(Index.isValid());
QModelIndex ProxyIndex = mpProxyModel->mapFromSource(Index);
mpUI->ResourceTableView->selectionModel()->select(ProxyIndex, QItemSelectionModel::ClearAndSelect);
mpUI->ResourceTableView->edit(ProxyIndex);
}
return true;
}
return false;
}
bool CResourceBrowser::Delete(QVector<CResourceEntry*> Resources, QVector<CVirtualDirectory*> Directories)
{
// Don't delete any resources/directories that are still referenced.
// This is kind of a hack but there's no good way to clear out these references right now.
QString ErrorPaths;
for (int DirIdx = 0; DirIdx < Directories.size(); DirIdx++)
{
if (!Directories[DirIdx]->IsSafeToDelete())
{
ErrorPaths += TO_QSTRING( Directories[DirIdx]->FullPath() ) + '\n';
Directories.removeAt(DirIdx);
DirIdx--;
}
}
for (int ResIdx = 0; ResIdx < Resources.size(); ResIdx++)
{
if (Resources[ResIdx]->IsLoaded() && Resources[ResIdx]->Resource()->IsReferenced())
{
ErrorPaths += TO_QSTRING( Resources[ResIdx]->CookedAssetPath(true) ) + '\n';
Resources.removeAt(ResIdx);
ResIdx--;
}
}
if (!ErrorPaths.isEmpty())
{
// Remove trailing newline
ErrorPaths.chop(1);
UICommon::ErrorMsg(this, QString("The following resources/directories are still referenced and cannot be deleted:\n\n%1")
.arg(ErrorPaths));
}
// Gather a complete list of resources in subdirectories
for (int DirIdx = 0; DirIdx < Directories.size(); DirIdx++)
{
CVirtualDirectory* pDir = Directories[DirIdx];
Resources.reserve( Resources.size() + pDir->NumResources() );
Directories.reserve( Directories.size() + pDir->NumSubdirectories() );
for (uint ResourceIdx = 0; ResourceIdx < pDir->NumResources(); ResourceIdx++)
Resources << pDir->ResourceByIndex(ResourceIdx);
for (uint SubdirIdx = 0; SubdirIdx < pDir->NumSubdirectories(); SubdirIdx++)
Directories << pDir->SubdirectoryByIndex(SubdirIdx);
}
// Exit if we have nothing to do.
if (Resources.isEmpty() && Directories.isEmpty())
return false;
// Allow the user to confirm before proceeding.
QString ConfirmMsg = QString("Are you sure you want to permanently delete ");
if (Resources.size() > 0)
{
ConfirmMsg += QString("%1 resource%2").arg(Resources.size()).arg(Resources.size() == 1 ? "" : "s");
if (Directories.size() > 0)
{
ConfirmMsg += " and ";
}
}
if (Directories.size() > 0)
{
ConfirmMsg += QString("%1 %2").arg(Directories.size()).arg(Directories.size() == 1 ? "directory" : "directories");
}
ConfirmMsg += "?";
if (UICommon::YesNoQuestion(this, "Warning", ConfirmMsg))
{
// Note that the undo stack will undo actions in the reverse order they are pushed
// So we need to push commands last that we want to be undone first
// We want to delete subdirectories first, then parent directories, then resources
mUndoStack.beginMacro("Delete");
mUndoStack.push( new CSaveStoreCommand(mpStore) );
// Delete resources first.
foreach (CResourceEntry* pEntry, Resources)
mUndoStack.push( new CDeleteResourceCommand(pEntry) );
// Now delete directories in reverse order (so subdirectories delete first)
for (int DirIdx = Directories.size()-1; DirIdx >= 0; DirIdx--)
{
CVirtualDirectory* pDir = Directories[DirIdx];
mUndoStack.push( new CDeleteDirectoryCommand(mpStore, pDir->Parent()->FullPath(), pDir->Name()) );
}
mUndoStack.push( new CSaveStoreCommand(mpStore) );
mUndoStack.endMacro();
return true;
}
else
return false;
}
void CResourceBrowser::OnSearchStringChanged(QString SearchString)
{
bool WasAssetList = InAssetListMode();
mSearching = !SearchString.isEmpty();
bool IsAssetList = InAssetListMode();
// Check if we need to change to/from asset list mode to display/stop displaying search results
if (WasAssetList != IsAssetList)
{
RefreshResources();
}
UpdateFilter();
}
void CResourceBrowser::OnDirectorySelectionChanged(const QModelIndex& rkNewIndex)
{
CVirtualDirectory *pDir = nullptr;
if (rkNewIndex.isValid())
pDir = mpDirectoryModel->IndexDirectory(rkNewIndex);
else
pDir = mpStore ? mpStore->RootDirectory() : nullptr;
SetActiveDirectory(pDir);
}
void CResourceBrowser::OnDoubleClickTable(QModelIndex Index)
{
QModelIndex SourceIndex = mpProxyModel->mapToSource(Index);
// Directory - switch to the selected directory
if (mpModel->IsIndexDirectory(SourceIndex))
{
CVirtualDirectory *pDir = mpModel->IndexDirectory(SourceIndex);
CVirtualDirectory *pOldDir = mpSelectedDir;
SetActiveDirectory(pDir);
if (pOldDir->Parent() == pDir)
SelectDirectory(pOldDir);
}
// Resource - open resource for editing
else
{
CResourceEntry *pEntry = mpModel->IndexEntry(SourceIndex);
gpEdApp->EditResource(pEntry);
}
}
void CResourceBrowser::OnResourceSelectionChanged(const QModelIndex& rkNewIndex)
{
QModelIndex SourceIndex = mpProxyModel->mapToSource(rkNewIndex);
mpSelectedEntry = mpModel->IndexEntry(SourceIndex);
emit SelectedResourceChanged(mpSelectedEntry);
}
void CResourceBrowser::FindAssetByID()
{
if (!mpStore)
return;
QString QStringAssetID = QInputDialog::getText(this, "Enter Asset ID", "Enter asset ID:");
TString StringAssetID = TO_TSTRING(QStringAssetID);
StringAssetID.RemoveWhitespace();
if (!StringAssetID.IsEmpty())
{
EGame Game = mpStore->Game();
EIDLength IDLength = CAssetID::GameIDLength(Game);
bool WasValid = false;
if (StringAssetID.IsHexString(false, IDLength * 2))
{
if (StringAssetID.StartsWith("0x", false))
StringAssetID = StringAssetID.ChopFront(2);
// Find the resource entry
if ( (IDLength == k32Bit && StringAssetID.Length() == 8) ||
(IDLength == k64Bit && StringAssetID.Length() == 16) )
{
CAssetID ID = (IDLength == k32Bit ? StringAssetID.ToInt32(16) : StringAssetID.ToInt64(16));
CResourceEntry *pEntry = mpStore->FindEntry(ID);
WasValid = true;
if (pEntry)
SelectResource(pEntry, true);
// User entered valid but unrecognized ID
else
UICommon::ErrorMsg(this, QString("Couldn't find any asset with ID %1").arg(QStringAssetID));
}
}
// User entered invalid string
if (!WasValid)
{
UICommon::ErrorMsg(this, "The entered string is not a valid asset ID!");
}
}
// User entered nothing, don't do anything
}
void CResourceBrowser::SetAssetIDDisplayEnabled(bool Enable)
{
mpDelegate->SetDisplayAssetIDs(Enable);
mpModel->RefreshAllIndices();
}
void CResourceBrowser::UpdateStore()
{
CGameProject *pProj = gpEdApp->ActiveProject();
CResourceStore *pProjStore = (pProj ? pProj->ResourceStore() : nullptr);
CResourceStore *pNewStore = (mEditorStore ? gpEditorStore : pProjStore);
if (mpStore != pNewStore)
{
mpStore = pNewStore;
// Clear search
mpUI->SearchBar->clear();
mSearching = false;
// Refresh project-specific UI
CreateAddMenu();
CreateFilterCheckboxes();
setEnabled(mpStore != nullptr);
// Refresh directory tree
mpDirectoryModel->SetRoot(mpStore ? mpStore->RootDirectory() : nullptr);
QModelIndex RootIndex = mpDirectoryModel->index(0, 0, QModelIndex());
mpUI->DirectoryTreeView->expand(RootIndex);
mpUI->DirectoryTreeView->clearSelection();
OnDirectorySelectionChanged(QModelIndex());
}
}
void CResourceBrowser::SetProjectStore()
{
mEditorStore = false;
UpdateStore();
}
void CResourceBrowser::SetEditorStore()
{
mEditorStore = true;
UpdateStore();
}
void CResourceBrowser::ImportPackageContentsList()
{
QStringList PathList = UICommon::OpenFilesDialog(this, "Open package contents list", "*.pak.contents.txt");
if (PathList.isEmpty()) return;
SetActiveDirectory(nullptr);
foreach(const QString& rkPath, PathList)
mpStore->ImportNamesFromPakContentsTxt(TO_TSTRING(rkPath), false);
RefreshResources();
RefreshDirectories();
}
void CResourceBrowser::GenerateAssetNames()
{
SetActiveDirectory(nullptr);
CProgressDialog Dialog("Generating asset names", true, true, this);
Dialog.DisallowCanceling();
Dialog.SetOneShotTask("Generating asset names");
// Temporarily set root to null to ensure the window doesn't access the resource store while we're running.
mpDirectoryModel->SetRoot(mpStore->RootDirectory());
QFuture<void> Future = QtConcurrent::run(&::GenerateAssetNames, mpStore->Project());
Dialog.WaitForResults(Future);
RefreshResources();
RefreshDirectories();
UICommon::InfoMsg(this, "Complete", "Asset name generation complete!");
}
void CResourceBrowser::ImportAssetNameMap()
{
CAssetNameMap Map( mpStore->Game() );
bool LoadSuccess = Map.LoadAssetNames();
if (!LoadSuccess)
{
UICommon::ErrorMsg(this, "Import failed; couldn't load asset name map!");
return;
}
else if (!Map.IsValid())
{
UICommon::ErrorMsg(this, "Import failed; the input asset name map is invalid! See the log for details.");
return;
}
SetActiveDirectory(nullptr);
for (CResourceIterator It(mpStore); It; ++It)
{
TString Dir, Name;
bool AutoDir, AutoName;
if (Map.GetNameInfo(It->ID(), Dir, Name, AutoDir, AutoName))
It->MoveAndRename(Dir, Name, AutoDir, AutoName);
}
mpStore->ConditionalSaveStore();
RefreshResources();
RefreshDirectories();
UICommon::InfoMsg(this, "Success", "New asset names imported successfully!");
}
void CResourceBrowser::ExportAssetNames()
{
QString OutFile = UICommon::SaveFileDialog(this, "Export asset name map", "*.xml",
gResourcesWritable ? *(gDataDir + "resources/gameinfo/") : "");
if (OutFile.isEmpty()) return;
TString OutFileStr = TO_TSTRING(OutFile);
CAssetNameMap NameMap(mpStore->Game());
if (FileUtil::Exists(OutFileStr))
{
bool LoadSuccess = NameMap.LoadAssetNames(OutFileStr);
if (!LoadSuccess || !NameMap.IsValid())
{
UICommon::ErrorMsg(this, "Unable to export; failed to load existing names from the original asset name map file! See the log for details.");
return;
}
}
NameMap.CopyFromStore(mpStore);
bool SaveSuccess = NameMap.SaveAssetNames(OutFileStr);
if (!SaveSuccess)
UICommon::ErrorMsg(this, "Failed to export asset names!");
else
UICommon::InfoMsg(this, "Success", "Asset names exported successfully!");
}
void CResourceBrowser::RebuildResourceDB()
{
if (UICommon::YesNoQuestion(this, "Rebuild resource database", "Are you sure you want to rebuild the resource database? This will take a while."))
{
gpEdApp->RebuildResourceDatabase();
}
}
void CResourceBrowser::ClearFilters()
{
ResetSearch();
ResetTypeFilter();
}
void CResourceBrowser::ResetSearch()
{
bool WasAssetList = InAssetListMode();
mpUI->SearchBar->clear();
mSearching = false;
bool IsAssetList = InAssetListMode();
if (IsAssetList != WasAssetList)
RefreshResources();
UpdateFilter();
}
void CResourceBrowser::ResetTypeFilter()
{
mpFilterAllBox->setChecked(true);
UpdateFilter();
}
void CResourceBrowser::OnFilterTypeBoxTicked(bool Checked)
{
// NOTE: there should only be one CResourceBrowser; if that ever changes for some reason change this to a member
static bool ReentrantGuard = false;
if (ReentrantGuard) return;
ReentrantGuard = true;
if (sender() == mpFilterAllBox)
{
if (!Checked && !mpProxyModel->HasTypeFilter())
mpFilterAllBox->setChecked(true);
else if (Checked)
{
foreach (const SResourceType& rkType, mTypeList)
{
rkType.pFilterCheckBox->setChecked(false);
mpProxyModel->SetTypeFilter(rkType.pTypeInfo, false);
}
}
}
else
{
foreach (const SResourceType& rkType, mTypeList)
{
if (rkType.pFilterCheckBox == sender())
{
mpProxyModel->SetTypeFilter(rkType.pTypeInfo, Checked);
break;
}
}
mpFilterAllBox->setChecked(!mpProxyModel->HasTypeFilter());
}
mpProxyModel->invalidate();
ReentrantGuard = false;
}
void CResourceBrowser::UpdateFilter()
{
QString SearchText = mpUI->SearchBar->text();
mSearching = !SearchText.isEmpty();
UpdateDescriptionLabel();
mpProxyModel->SetSearchString( TO_TSTRING(mpUI->SearchBar->text()) );
mpProxyModel->invalidate();
// not sure why I need to do this here? but the resize mode seems to get reset otherwise
mpUI->ResourceTableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
}
void CResourceBrowser::UpdateUndoActionStates()
{
// Make sure that the undo actions are only enabled when the table view has focus.
// This is to prevent them from conflicting with world editor undo/redo actions.
bool HasFocus = (mpUI->ResourceTableView->hasFocus());
mpUndoAction->setEnabled( HasFocus && mUndoStack.canUndo() );
mpRedoAction->setEnabled( HasFocus && mUndoStack.canRedo() );
}
void CResourceBrowser::Undo()
{
mUndoStack.undo();
UpdateUndoActionStates();
}
void CResourceBrowser::Redo()
{
mUndoStack.redo();
UpdateUndoActionStates();
}