1125 lines
36 KiB
C++
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();
|
|
}
|