diff --git a/externals/LibCommon b/externals/LibCommon index 5c3bfbe5..3c6a4074 160000 --- a/externals/LibCommon +++ b/externals/LibCommon @@ -1 +1 @@ -Subproject commit 5c3bfbe57f6ef8a300933afdc053a445cabd5c7c +Subproject commit 3c6a40742551d7afd0737d1293d036df69f34ec6 diff --git a/src/Core/GameProject/CGameProject.cpp b/src/Core/GameProject/CGameProject.cpp index 5eb01c4d..f044a451 100644 --- a/src/Core/GameProject/CGameProject.cpp +++ b/src/Core/GameProject/CGameProject.cpp @@ -297,6 +297,15 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti gpResourceStore = pOldStore; } + // Create hidden files directory, if needed + TString HiddenDir = pProj->HiddenFilesDir(); + + if (!FileUtil::Exists(HiddenDir)) + { + FileUtil::MakeDirectory(HiddenDir); + FileUtil::MarkHidden(HiddenDir, true); + } + pProj->mpAudioManager->LoadAssets(); pProj->mpTweakManager->LoadTweaks(); return pProj; diff --git a/src/Core/GameProject/CGameProject.h b/src/Core/GameProject/CGameProject.h index d9f22b19..8b0dd023 100644 --- a/src/Core/GameProject/CGameProject.h +++ b/src/Core/GameProject/CGameProject.h @@ -84,6 +84,7 @@ public: // Directory Handling inline TString ProjectRoot() const { return mProjectRoot; } inline TString ProjectPath() const { return mProjectRoot + FileUtil::SanitizeName(mProjectName, false) + ".prj"; } + inline TString HiddenFilesDir() const { return mProjectRoot + ".project/"; } inline TString DiscDir(bool Relative) const { return Relative ? "Disc/" : mProjectRoot + "Disc/"; } inline TString PackagesDir(bool Relative) const { return Relative ? "Packages/" : mProjectRoot + "Packages/"; } inline TString ResourcesDir(bool Relative) const { return Relative ? "Resources/" : mProjectRoot + "Resources/"; } diff --git a/src/Core/GameProject/CResourceEntry.cpp b/src/Core/GameProject/CResourceEntry.cpp index 0fbeddae..e2229c15 100644 --- a/src/Core/GameProject/CResourceEntry.cpp +++ b/src/Core/GameProject/CResourceEntry.cpp @@ -113,6 +113,9 @@ bool CResourceEntry::LoadMetadata() bool CResourceEntry::SaveMetadata(bool ForceSave /*= false*/) { + // Make sure we aren't saving a deleted resource + ASSERT( !HasFlag(EResEntryFlag::MarkedForDeletion) ); + if (mMetadataDirty || ForceSave) { TString Path = MetadataFilePath(); @@ -491,6 +494,9 @@ bool CResourceEntry::CanMoveTo(const TString& rkDir, const TString& rkName) bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName, bool IsAutoGenDir /*= false*/, bool IsAutoGenName /*= false*/) { + // Make sure we are not moving a deleted resource. + ASSERT( !IsMarkedForDeletion() ); + if (!CanMoveTo(rkDir, rkName)) return false; // Store old paths @@ -501,7 +507,15 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName, TString OldMetaPath = MetadataFilePath(); // Set new directory and name - CVirtualDirectory *pNewDir = mpStore->GetVirtualDirectory(rkDir, true); + bool DirAlreadyExisted = true; + CVirtualDirectory *pNewDir = mpStore->GetVirtualDirectory(rkDir, false); + + if (!pNewDir) + { + pNewDir = mpStore->GetVirtualDirectory(rkDir, true); + DirAlreadyExisted = false; + } + if (pNewDir == mpDirectory && rkName == mName) return false; // Check if we can legally move to this spot @@ -580,7 +594,7 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName, // If we succeeded, finish the move if (FSMoveSuccess) { - if (mpDirectory != pOldDir) + if (mpDirectory != pOldDir && pOldDir != nullptr) { FSMoveSuccess = pOldDir->RemoveChildResource(this); ASSERT(FSMoveSuccess == true); // this shouldn't be able to fail @@ -605,7 +619,11 @@ bool CResourceEntry::MoveAndRename(const TString& rkDir, const TString& rkName, errorf("MOVE FAILED: %s", *MoveFailReason); mpDirectory = pOldDir; mName = OldName; - mpStore->ConditionalDeleteDirectory(pNewDir, false); + + if (!DirAlreadyExisted) + { + mpStore->ConditionalDeleteDirectory(pNewDir, true); + } if (FileUtil::Exists(NewRawPath)) FileUtil::MoveFile(NewRawPath, OldRawPath); @@ -627,6 +645,96 @@ bool CResourceEntry::Rename(const TString& rkName, bool IsAutoGenName /*= false* return MoveAndRename(mpDirectory->FullPath(), rkName, false, IsAutoGenName); } +void CResourceEntry::MarkDeleted(bool InDeleted) +{ + // Flags resource for future deletion. "Deleted" resources remain in memory (which + // allows them to easily be un-deleted) but cannot be looked up in the resource + // store and will not save back out to the resource database. Their file data is + // stored in a temporary directory, which allows them to be moved back if the user + // un-does the deletion. + if (IsMarkedForDeletion() != InDeleted) + { + SetFlagEnabled(EResEntryFlag::MarkedForDeletion, InDeleted); + + // Restore old name/directory if un-deleting + if (!InDeleted) + { + // Our directory path is stored in the Name field - see below for explanation + int NameEnd = mName.IndexOf('|'); + ASSERT( NameEnd != -1 ); + + TString DirPath = mName.ChopFront(NameEnd + 1); + mName = mName.ChopBack( mName.Size() - NameEnd); + mpDirectory = mpStore->GetVirtualDirectory( DirPath, true ); + ASSERT( mpDirectory != nullptr ); + mpDirectory->AddChild("", this); + } + + TString CookedPath = CookedAssetPath(); + TString RawPath = RawAssetPath(); + TString MetaPath = MetadataFilePath(); + + TString PathBase = mpStore->DeletedResourcePath() + mID.ToString() + "."; + TString DelCookedPath = PathBase + CookedExtension().ToString(); + TString DelRawPath = DelCookedPath + ".rsraw"; + TString DelMetaPath = DelCookedPath + ".rsmeta"; + + // If we are deleting... + if (InDeleted) + { + // Temporarily store our directory path in the name string. + // This is a hack, but we can't store the directory pointer because it may have been + // deleted and remade by the user by the time the resource is un-deleted, which + // means it is not safe to access later. Separating the name and the path with + // the '|' character is safe because this character is not allowed in filenames + // (which is enforced in FileUtil::IsValidName()). + mName = mName + "|" + mpDirectory->FullPath(); + + // Remove from parent directory. + mpDirectory->RemoveChildResource(this); + mpDirectory = nullptr; + + // Move any resource files out of the project into a temporary folder. + FileUtil::MakeDirectory(DelMetaPath.GetFileDirectory()); + + if (FileUtil::Exists(MetaPath)) + { + FileUtil::MoveFile(MetaPath, DelMetaPath); + } + if (FileUtil::Exists(RawPath)) + { + FileUtil::MoveFile(RawPath, DelRawPath); + } + if (FileUtil::Exists(CookedPath)) + { + FileUtil::MoveFile(CookedPath, DelCookedPath); + } + } + // If we are un-deleting... + else + { + // Move any resource files out of the temporary folder back into the project. + FileUtil::MakeDirectory(MetaPath.GetFileDirectory()); + + if (FileUtil::Exists(DelMetaPath)) + { + FileUtil::MoveFile(DelMetaPath, MetaPath); + } + if (FileUtil::Exists(DelRawPath)) + { + FileUtil::MoveFile(DelRawPath, RawPath); + } + if (FileUtil::Exists(DelCookedPath)) + { + FileUtil::MoveFile(DelCookedPath, CookedPath); + } + } + + mpStore->SetCacheDirty(); + debugf("%s FOR DELETION: [%s] %s", InDeleted ? "MARKED" : "UNMARKED", *ID().ToString(), *CookedPath.GetFileName()); + } +} + CGameProject* CResourceEntry::Project() const { return mpStore ? mpStore->Project() : nullptr; diff --git a/src/Core/GameProject/CResourceEntry.h b/src/Core/GameProject/CResourceEntry.h index ed285601..21e76a48 100644 --- a/src/Core/GameProject/CResourceEntry.h +++ b/src/Core/GameProject/CResourceEntry.h @@ -21,6 +21,7 @@ enum class EResEntryFlag HasBeenModified = 0x00000008, // Resource has been modified and resaved by the user AutoResName = 0x00000010, // Resource name is auto-generated AutoResDir = 0x00000020, // Resource directory name is auto-generated + MarkedForDeletion = 0x00000040, // Resource has been marked for deletion by the user }; DECLARE_FLAGS(EResEntryFlag, FResEntryFlags) @@ -76,6 +77,7 @@ public: bool MoveAndRename(const TString& rkDir, const TString& rkName, bool IsAutoGenDir = false, bool IsAutoGenName = false); bool Move(const TString& rkDir, bool IsAutoGenDir = false); bool Rename(const TString& rkName, bool IsAutoGenName = false); + void MarkDeleted(bool InDeleted); CGameProject* Project() const; EGame Game() const; @@ -90,6 +92,7 @@ public: inline void SetHidden(bool Hidden) { SetFlagEnabled(EResEntryFlag::Hidden, Hidden); } inline bool HasFlag(EResEntryFlag Flag) const { return mFlags.HasFlag(Flag); } inline bool IsHidden() const { return HasFlag(EResEntryFlag::Hidden); } + inline bool IsMarkedForDeletion() const { return HasFlag(EResEntryFlag::MarkedForDeletion); } inline bool IsLoaded() const { return mpResource != nullptr; } inline bool IsCategorized() const { return mpDirectory && !mpDirectory->FullPath().CaseInsensitiveCompare( mpStore->DefaultResourceDirPath() ); } diff --git a/src/Core/GameProject/CResourceIterator.h b/src/Core/GameProject/CResourceIterator.h index 1535a32b..6951da25 100644 --- a/src/Core/GameProject/CResourceIterator.h +++ b/src/Core/GameProject/CResourceIterator.h @@ -22,12 +22,16 @@ public: virtual CResourceEntry* Next() { - if (mIter != mpkStore->mResourceEntries.end()) + do { - mpCurEntry = mIter->second; - mIter++; + if (mIter != mpkStore->mResourceEntries.end()) + { + mpCurEntry = mIter->second; + mIter++; + } + else mpCurEntry = nullptr; } - else mpCurEntry = nullptr; + while (mpCurEntry && mpCurEntry->IsMarkedForDeletion()); return mpCurEntry; } diff --git a/src/Core/GameProject/CResourceStore.cpp b/src/Core/GameProject/CResourceStore.cpp index b30598ea..59abf94d 100644 --- a/src/Core/GameProject/CResourceStore.cpp +++ b/src/Core/GameProject/CResourceStore.cpp @@ -66,6 +66,22 @@ bool CResourceStore::SerializeDatabaseCache(IArchive& rArc) { // Serialize resources uint32 ResourceCount = mResourceEntries.size(); + + if (rArc.IsWriter()) + { + // Make sure deleted resources aren't included in the count. + // We can't use CResourceIterator because it skips MarkedForDeletion resources. + for (auto Iter = mResourceEntries.begin(); Iter != mResourceEntries.end(); Iter++) + { + CResourceEntry* pEntry = Iter->second; + + if (pEntry->IsMarkedForDeletion()) + { + ResourceCount--; + } + } + } + rArc << SerialParameter("ResourceCount", ResourceCount); if (rArc.IsReader()) @@ -85,10 +101,13 @@ bool CResourceStore::SerializeDatabaseCache(IArchive& rArc) { for (CResourceIterator It(this); It; ++It) { - if (rArc.ParamBegin("Resource", 0)) + if (!It->IsMarkedForDeletion()) { - It->SerializeEntryInfo(rArc, false); - rArc.ParamEnd(); + if (rArc.ParamBegin("Resource", 0)) + { + It->SerializeEntryInfo(rArc, false); + rArc.ParamEnd(); + } } } } @@ -140,18 +159,22 @@ bool CResourceStore::LoadDatabaseCache() } else { - // Database is succesfully loaded at this point + // Database is successfully loaded at this point if (mpProj) + { ASSERT(mpProj->Game() == Reader.Game()); + } + + mGame = Reader.Game(); } - mGame = Reader.Game(); return true; } bool CResourceStore::SaveDatabaseCache() { TString Path = DatabasePath(); + debugf("Saving database cache..."); CBasicBinaryWriter Writer(Path, FOURCC('CACH'), 0, mGame); @@ -182,6 +205,14 @@ void CResourceStore::SetProject(CGameProject *pProj) mDatabasePath = mpProj->ProjectRoot(); mpDatabaseRoot = new CVirtualDirectory(this); mGame = mpProj->Game(); + + // Clear deleted files from previous runs + TString DeletedPath = DeletedResourcePath(); + + if (FileUtil::Exists(DeletedPath)) + { + FileUtil::ClearDirectory(DeletedPath); + } } } @@ -215,6 +246,14 @@ void CResourceStore::CloseProject() It = mResourceEntries.erase(It); } + // Clear deleted files from previous runs + TString DeletedPath = DeletedResourcePath(); + + if (FileUtil::Exists(DeletedPath)) + { + FileUtil::ClearDirectory(DeletedPath); + } + delete mpDatabaseRoot; mpDatabaseRoot = nullptr; mpProj = nullptr; @@ -256,12 +295,27 @@ TString CResourceStore::DefaultResourceDirPath() const return StaticDefaultResourceDirPath( mGame ); } +TString CResourceStore::DeletedResourcePath() const +{ + return mpProj->HiddenFilesDir() / "delete/"; +} + CResourceEntry* CResourceStore::FindEntry(const CAssetID& rkID) const { - if (!rkID.IsValid()) return nullptr; - auto Found = mResourceEntries.find(rkID); - if (Found == mResourceEntries.end()) return nullptr; - else return Found->second; + if (rkID.IsValid()) + { + auto Found = mResourceEntries.find(rkID); + + if (Found != mResourceEntries.end()) + { + CResourceEntry* pEntry = Found->second; + + if (!pEntry->IsMarkedForDeletion()) + return pEntry; + } + } + + return nullptr; } CResourceEntry* CResourceStore::FindEntry(const TString& rkPath) const @@ -411,6 +465,8 @@ CResourceEntry* CResourceStore::CreateNewResource(const CAssetID& rkID, EResourc { TrackLoadedResource(pEntry); } + + debugf("CREATED NEW RESOURCE: [%s] %s", *rkID.ToString(), *pEntry->CookedAssetPath()); } else diff --git a/src/Core/GameProject/CResourceStore.h b/src/Core/GameProject/CResourceStore.h index d721484a..85cf3a7d 100644 --- a/src/Core/GameProject/CResourceStore.h +++ b/src/Core/GameProject/CResourceStore.h @@ -52,6 +52,7 @@ public: void CreateVirtualDirectory(const TString& rkPath); void ConditionalDeleteDirectory(CVirtualDirectory *pDir, bool Recurse); TString DefaultResourceDirPath() const; + TString DeletedResourcePath() const; bool IsResourceRegistered(const CAssetID& rkID) const; CResourceEntry* CreateNewResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName); diff --git a/src/Core/GameProject/CVirtualDirectory.cpp b/src/Core/GameProject/CVirtualDirectory.cpp index 9fd2931d..27adc863 100644 --- a/src/Core/GameProject/CVirtualDirectory.cpp +++ b/src/Core/GameProject/CVirtualDirectory.cpp @@ -46,6 +46,28 @@ bool CVirtualDirectory::IsDescendantOf(CVirtualDirectory *pDir) const return (this == pDir) || (mpParent && pDir && (mpParent == pDir || mpParent->IsDescendantOf(pDir))); } +bool CVirtualDirectory::IsSafeToDelete() const +{ + // Return false if we contain any referenced assets. + for (CResourceEntry* pEntry : mResources) + { + if (pEntry->IsLoaded() && pEntry->Resource()->IsReferenced()) + { + return false; + } + } + + for (CVirtualDirectory* pSubdir : mSubdirectories) + { + if (!pSubdir->IsSafeToDelete()) + { + return false; + } + } + + return true; +} + TString CVirtualDirectory::FullPath() const { if (IsRoot()) diff --git a/src/Core/GameProject/CVirtualDirectory.h b/src/Core/GameProject/CVirtualDirectory.h index d5f6d2e4..fc0550be 100644 --- a/src/Core/GameProject/CVirtualDirectory.h +++ b/src/Core/GameProject/CVirtualDirectory.h @@ -26,6 +26,7 @@ public: bool IsEmpty(bool CheckFilesystem) const; bool IsDescendantOf(CVirtualDirectory *pDir) const; + bool IsSafeToDelete() const; TString FullPath() const; TString AbsolutePath() const; CVirtualDirectory* GetRoot(); diff --git a/src/Editor/CTweakEditor.cpp b/src/Editor/CTweakEditor.cpp index 5de8f1b0..a36d3160 100644 --- a/src/Editor/CTweakEditor.cpp +++ b/src/Editor/CTweakEditor.cpp @@ -64,19 +64,6 @@ bool CTweakEditor::Save() } } -void CTweakEditor::showEvent(QShowEvent* pEvent) -{ - // Perform first-time UI initialization - // Property view cannot initialize correctly until first show due to window width not being configured - if (!mHasBeenShown) - { - mpUI->PropertyView->InitColumnWidths(0.6f, 0.3f); - mHasBeenShown = true; - } - - IEditor::showEvent(pEvent); -} - void CTweakEditor::SetActiveTweakData(CTweakData* pTweakData) { for( int TweakIdx = 0; TweakIdx < mTweakAssets.size(); TweakIdx++ ) diff --git a/src/Editor/CTweakEditor.h b/src/Editor/CTweakEditor.h index 72f135ed..b1ed6a3f 100644 --- a/src/Editor/CTweakEditor.h +++ b/src/Editor/CTweakEditor.h @@ -29,7 +29,6 @@ public: bool HasTweaks(); virtual bool Save() override; - virtual void showEvent(QShowEvent* pEvent) override; public slots: void SetActiveTweakData(CTweakData* pTweakData); diff --git a/src/Editor/Editor.pro b/src/Editor/Editor.pro index 0f37ed54..528f3191 100644 --- a/src/Editor/Editor.pro +++ b/src/Editor/Editor.pro @@ -203,7 +203,9 @@ HEADERS += \ Undo/CEditIntrinsicPropertyCommand.h \ Undo/TSerializeUndoCommand.h \ StringEditor/CStringMimeData.h \ - ScanEditor/CScanEditor.h + ScanEditor/CScanEditor.h \ + Undo/ICreateDeleteResourceCommand.h \ + Undo/CSaveStoreCommand.h # Source Files SOURCES += \ diff --git a/src/Editor/PropertyEdit/CPropertyView.cpp b/src/Editor/PropertyEdit/CPropertyView.cpp index c267198d..ac3853fb 100644 --- a/src/Editor/PropertyEdit/CPropertyView.cpp +++ b/src/Editor/PropertyEdit/CPropertyView.cpp @@ -79,8 +79,20 @@ bool CPropertyView::event(QEvent *pEvent) pEvent->ignore(); return true; } + else if (pEvent->type() == QEvent::Resize && !isVisible()) + { + resizeColumnToContents(0); + } - else return QTreeView::event(pEvent); + return QTreeView::event(pEvent); +} + +int CPropertyView::sizeHintForColumn(int Column) const +{ + if (Column == 0) + return width() * 0.6f; + else + return width() * 0.4f; } void CPropertyView::SetEditor(IEditor* pEditor) @@ -88,13 +100,6 @@ void CPropertyView::SetEditor(IEditor* pEditor) mpDelegate->SetEditor(pEditor); } -void CPropertyView::InitColumnWidths(float NameColumnPercentage, float ValueColumnPercentage) -{ - header()->resizeSection(0, width() * NameColumnPercentage); - header()->resizeSection(1, width() * ValueColumnPercentage); - header()->setSectionResizeMode(1, QHeaderView::Fixed); -} - void CPropertyView::ClearProperties() { mpObject = nullptr; diff --git a/src/Editor/PropertyEdit/CPropertyView.h b/src/Editor/PropertyEdit/CPropertyView.h index ca01c252..7ed04561 100644 --- a/src/Editor/PropertyEdit/CPropertyView.h +++ b/src/Editor/PropertyEdit/CPropertyView.h @@ -25,8 +25,9 @@ public: CPropertyView(QWidget* pParent = 0); void setModel(QAbstractItemModel* pModel); bool event(QEvent* pEvent); + int sizeHintForColumn(int Column) const; + void SetEditor(IEditor* pEditor); - void InitColumnWidths(float NameColumnPercentage, float ValueColumnPercentage); void ClearProperties(); void SetIntrinsicProperties(CStructRef InProperties); void SetInstance(CScriptObject* pObj); diff --git a/src/Editor/ResourceBrowser/CResourceBrowser.cpp b/src/Editor/ResourceBrowser/CResourceBrowser.cpp index 2dcfb7f2..68411585 100644 --- a/src/Editor/ResourceBrowser/CResourceBrowser.cpp +++ b/src/Editor/ResourceBrowser/CResourceBrowser.cpp @@ -8,7 +8,9 @@ #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 #include @@ -346,7 +348,11 @@ bool CResourceBrowser::RenameResource(CResourceEntry *pEntry, const TString& rkN } // 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; } @@ -369,7 +375,11 @@ bool CResourceBrowser::RenameDirectory(CVirtualDirectory *pDir, const TString& r } // 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; } @@ -431,6 +441,7 @@ bool CResourceBrowser::MoveResources(const QList& rkResources, 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) ); @@ -438,43 +449,59 @@ bool CResourceBrowser::MoveResources(const QList& rkResources, foreach (CResourceEntry *pEntry, ValidResources) mUndoStack.push( new CMoveResourceCommand(pEntry, pNewDir) ); + mUndoStack.push( new CSaveStoreCommand(mpStore) ); mUndoStack.endMacro(); } return true; } -CResourceEntry* CResourceBrowser::CreateNewResource(EResourceType Type) +CResourceEntry* CResourceBrowser::CreateNewResource(EResourceType Type, + TString Name /*= ""*/, + CVirtualDirectory* pDir /*= nullptr*/, + CAssetID ID /*= CAssetID()*/) { - // Create new asset ID. Sanity check to make sure the ID is unused. - CAssetID NewAssetID; - - while (!NewAssetID.IsValid() || mpStore->FindEntry(NewAssetID) != nullptr) + if (!pDir) { - NewAssetID = CAssetID::RandomID( mpStore->Game() ); + 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 = TString::Format( + TString BaseName = Name; + + if (BaseName.IsEmpty()) + { + BaseName = TString::Format( "New %s", *CResTypeInfo::FindTypeInfo(Type)->TypeName() ); + } - TString Name = BaseName; + Name = BaseName; int Num = 0; - while (mpSelectedDir->FindChildResource(Name, Type) != nullptr) + while (pDir->FindChildResource(Name, Type) != nullptr) { Num++; Name = TString::Format("%s (%d)", *BaseName, Num); } - emit ResourceAboutToBeCreated(mpSelectedDir); - // Create the actual resource - CResourceEntry* pEntry = mpStore->CreateNewResource(NewAssetID, Type, mpSelectedDir->FullPath(), Name); - pEntry->Save(); + CResourceEntry* pEntry = mpStore->CreateNewResource(ID, Type, pDir->FullPath(), Name); - emit ResourceCreated(pEntry); + // 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); @@ -617,8 +644,12 @@ bool CResourceBrowser::CreateDirectory() } // 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); @@ -641,28 +672,101 @@ bool CResourceBrowser::CreateDirectory() return false; } -bool CResourceBrowser::DeleteDirectories(const QList& rkDirs) +bool CResourceBrowser::Delete(QVector Resources, QVector Directories) { - QList DeletableDirs; + // 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; - foreach (CVirtualDirectory *pDir, rkDirs) + for (int DirIdx = 0; DirIdx < Directories.size(); DirIdx++) { - if (pDir && pDir->IsEmpty(true)) - DeletableDirs << pDir; + if (!Directories[DirIdx]->IsSafeToDelete()) + { + ErrorPaths += TO_QSTRING( Directories[DirIdx]->FullPath() ) + '\n'; + Directories.removeAt(DirIdx); + DirIdx--; + } } - if (DeletableDirs.size() > 0) + for (int ResIdx = 0; ResIdx < Resources.size(); ResIdx++) { - mUndoStack.beginMacro("Delete Directories"); + if (Resources[ResIdx]->IsLoaded() && Resources[ResIdx]->Resource()->IsReferenced()) + { + ErrorPaths += TO_QSTRING( Resources[ResIdx]->CookedAssetPath(true) ) + '\n'; + Resources.removeAt(ResIdx); + ResIdx--; + } + } - foreach (CVirtualDirectory *pDir, DeletableDirs) + 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; + else + return false; } void CResourceBrowser::OnSearchStringChanged(QString SearchString) diff --git a/src/Editor/ResourceBrowser/CResourceBrowser.h b/src/Editor/ResourceBrowser/CResourceBrowser.h index 5093d976..73a61dc1 100644 --- a/src/Editor/ResourceBrowser/CResourceBrowser.h +++ b/src/Editor/ResourceBrowser/CResourceBrowser.h @@ -72,7 +72,10 @@ public: bool RenameDirectory(CVirtualDirectory *pDir, const TString& rkNewName); bool MoveResources(const QList& rkResources, const QList& rkDirectories, CVirtualDirectory *pNewDir); - CResourceEntry* CreateNewResource(EResourceType Type); + CResourceEntry* CreateNewResource(EResourceType Type, + TString Name = "", + CVirtualDirectory* pDir = nullptr, + CAssetID ID = CAssetID()); // Interface bool eventFilter(QObject *pWatched, QEvent *pEvent); @@ -94,7 +97,7 @@ public slots: void OnSortModeChanged(int Index); void OnCreateAssetAction(); bool CreateDirectory(); - bool DeleteDirectories(const QList& rkDirs); + bool Delete(QVector Resources, QVector Directories); void OnSearchStringChanged(QString SearchString); void OnDirectorySelectionChanged(const QModelIndex& rkNewIndex); void OnDoubleClickTable(QModelIndex Index); diff --git a/src/Editor/ResourceBrowser/CResourceTableContextMenu.cpp b/src/Editor/ResourceBrowser/CResourceTableContextMenu.cpp index 37825da2..c1358274 100644 --- a/src/Editor/ResourceBrowser/CResourceTableContextMenu.cpp +++ b/src/Editor/ResourceBrowser/CResourceTableContextMenu.cpp @@ -1,6 +1,9 @@ #include "CResourceTableContextMenu.h" #include "CResourceBrowser.h" #include "Editor/CEditorApplication.h" + +#include + #include CResourceTableContextMenu::CResourceTableContextMenu(CResourceBrowser *pBrowser, QTableView *pView, CResourceTableModel *pModel, CResourceProxyModel *pProxy) @@ -70,6 +73,17 @@ void CResourceTableContextMenu::InitMenu() QMenu* pCreate = addMenu("Create..."); mpBrowser->AddCreateAssetMenuActions(pCreate); + + // Asset-specific + if (mpClickedEntry) + { + switch (mpClickedEntry->ResourceType()) + { + case EResourceType::StringTable: + addAction("Create Scan", this, SLOT(CreateSCAN())); + break; + } + } } void CResourceTableContextMenu::ShowMenu(const QPoint& rkPos) @@ -198,43 +212,18 @@ void CResourceTableContextMenu::ShowDependencies() void CResourceTableContextMenu::Delete() { // Create confirmation message - uint NumResources = 0, NumDirectories = 0; + QVector Resources; + QVector Directories; foreach (const QModelIndex& kIndex, mSelectedIndexes) { if (mpModel->IsIndexDirectory(kIndex)) - NumDirectories++; + Directories << mpModel->IndexDirectory(kIndex); else - NumResources++; + Resources << mpModel->IndexEntry(kIndex); } - if (NumResources == 0 && NumDirectories == 0) - return; - - QString ConfirmMsg = QString("Are you sure you want to permanently delete "); - - if (NumResources > 0) - { - ConfirmMsg += QString("%d resource%s").arg(NumResources).arg(NumResources == 1 ? "" : "s"); - - if (NumDirectories > 0) - { - ConfirmMsg += " and "; - } - } - if (NumDirectories > 0) - { - ConfirmMsg += QString("%d %s").arg(NumDirectories).arg(NumDirectories == 1 ? "directory" : "directories"); - } - - // Allow the user to confirm the action before performing it - if (UICommon::YesNoQuestion(mpBrowser, "Warning", ConfirmMsg)) - { - //@todo this is wrong lol - QList List; - List << mpClickedDirectory; - mpBrowser->DeleteDirectories(List); - } + mpBrowser->Delete(Resources, Directories); } void CResourceTableContextMenu::CopyName() @@ -258,3 +247,22 @@ void CResourceTableContextMenu::CopyID() ASSERT(mpClickedEntry); gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedEntry->ID().ToString()) ); } + + +// Asset Specific +void CResourceTableContextMenu::CreateSCAN() +{ + // Create a SCAN asset to go along with a selected STRG asset + ASSERT( mpClickedEntry && mpClickedEntry->ResourceType() == EResourceType::StringTable ); + + CResourceEntry* pNewEntry = mpBrowser->CreateNewResource(EResourceType::Scan, + mpClickedEntry->Name(), + mpClickedEntry->Directory()); + + if (pNewEntry) + { + CScan* pScan = (CScan*) pNewEntry->Load(); + pScan->ScanStringPropertyRef().Set( mpClickedEntry->ID() ); + pNewEntry->Save(); + } +} diff --git a/src/Editor/ResourceBrowser/CResourceTableContextMenu.h b/src/Editor/ResourceBrowser/CResourceTableContextMenu.h index ffbfeaa9..68137c5b 100644 --- a/src/Editor/ResourceBrowser/CResourceTableContextMenu.h +++ b/src/Editor/ResourceBrowser/CResourceTableContextMenu.h @@ -41,6 +41,9 @@ public slots: void CopyName(); void CopyPath(); void CopyID(); + + // Asset Specific + void CreateSCAN(); }; #endif // CRESOURCETABLECONTEXTMENU_H diff --git a/src/Editor/ResourceBrowser/CResourceTableModel.cpp b/src/Editor/ResourceBrowser/CResourceTableModel.cpp index bbda5b2d..a100fbec 100644 --- a/src/Editor/ResourceBrowser/CResourceTableModel.cpp +++ b/src/Editor/ResourceBrowser/CResourceTableModel.cpp @@ -8,6 +8,7 @@ CResourceTableModel::CResourceTableModel(CResourceBrowser *pBrowser, QObject *pP , mIsDisplayingUserEntryList(false) { connect(pBrowser, SIGNAL(ResourceCreated(CResourceEntry*)), this, SLOT(CheckAddResource(CResourceEntry*))); + connect(pBrowser, SIGNAL(ResourceAboutToBeDeleted(CResourceEntry*)), this, SLOT(CheckRemoveResource(CResourceEntry*))); connect(pBrowser, SIGNAL(DirectoryCreated(CVirtualDirectory*)), this, SLOT(CheckAddDirectory(CVirtualDirectory*))); connect(pBrowser, SIGNAL(DirectoryAboutToBeDeleted(CVirtualDirectory*)), this, SLOT(CheckRemoveDirectory(CVirtualDirectory*))); connect(pBrowser, SIGNAL(ResourceMoved(CResourceEntry*,CVirtualDirectory*,TString)), this, SLOT(OnResourceMoved(CResourceEntry*,CVirtualDirectory*,TString))); @@ -309,8 +310,8 @@ void CResourceTableModel::CheckRemoveResource(CResourceEntry *pEntry) if (Index != -1) { - Index += mDirectories.size(); - beginRemoveRows(QModelIndex(), Index, Index); + int RowIndex = Index + mDirectories.size(); + beginRemoveRows(QModelIndex(), RowIndex, RowIndex); mEntries.removeAt(Index); endRemoveRows(); } diff --git a/src/Editor/ResourceBrowser/CResourceTableView.cpp b/src/Editor/ResourceBrowser/CResourceTableView.cpp index 9442f07c..bfa76826 100644 --- a/src/Editor/ResourceBrowser/CResourceTableView.cpp +++ b/src/Editor/ResourceBrowser/CResourceTableView.cpp @@ -44,34 +44,20 @@ void CResourceTableView::DeleteSelected() // Figure out which indices can actually be deleted CResourceProxyModel *pProxy = static_cast(model()); CResourceTableModel *pModel = static_cast(pProxy->sourceModel()); - QList DirsToDelete; - bool HasNonEmptyDirSelected = false; + QVector ResourcesToDelete; + QVector DirsToDelete; foreach (QModelIndex Index, List) { QModelIndex SourceIndex = pProxy->mapToSource(Index); if (pModel->IsIndexDirectory(SourceIndex)) - { - CVirtualDirectory *pDir = pModel->IndexDirectory(SourceIndex); + DirsToDelete << pModel->IndexDirectory(SourceIndex); + else + ResourcesToDelete << pModel->IndexEntry(SourceIndex); - if (pDir) - { - if (pDir->IsEmpty(true)) - DirsToDelete << pDir; - else - HasNonEmptyDirSelected = true; - } - } - } - - // Let the user know if all selected directories are non empty - if (HasNonEmptyDirSelected && DirsToDelete.isEmpty()) - { - UICommon::ErrorMsg(parentWidget(), "Unable to delete; one or more of the selected directories is non-empty."); - return; } // Delete - gpEdApp->ResourceBrowser()->DeleteDirectories(DirsToDelete); + gpEdApp->ResourceBrowser()->Delete(ResourcesToDelete, DirsToDelete); } diff --git a/src/Editor/Undo/CSaveStoreCommand.h b/src/Editor/Undo/CSaveStoreCommand.h new file mode 100644 index 00000000..20e48a98 --- /dev/null +++ b/src/Editor/Undo/CSaveStoreCommand.h @@ -0,0 +1,26 @@ +#ifndef CSAVESTORECOMMAND_H +#define CSAVESTORECOMMAND_H + +#include "IUndoCommand.h" +#include + +/** Command that calls ConditionalSaveStore on a resource store. + * This is meant to be added to undo macros that modify the resource store + * in order to trigger the store to resave when the macro is complete. + */ +class CSaveStoreCommand : public IUndoCommand +{ + CResourceStore* mpStore; + +public: + CSaveStoreCommand(CResourceStore* pInStore) + : IUndoCommand("Save Store") + , mpStore(pInStore) + {} + + virtual void undo() override { mpStore->ConditionalSaveStore(); } + virtual void redo() override { mpStore->ConditionalSaveStore(); } + virtual bool AffectsCleanState() const override { return false; } +}; + +#endif // CSAVESTORECOMMAND_H diff --git a/src/Editor/Undo/ICreateDeleteDirectoryCommand.h b/src/Editor/Undo/ICreateDeleteDirectoryCommand.h index 56802162..d2d635a4 100644 --- a/src/Editor/Undo/ICreateDeleteDirectoryCommand.h +++ b/src/Editor/Undo/ICreateDeleteDirectoryCommand.h @@ -16,8 +16,8 @@ protected: CVirtualDirectory *mpDir; public: - ICreateDeleteDirectoryCommand(CResourceStore *pStore, TString ParentPath, TString DirName) - : IUndoCommand("Create Directory") + ICreateDeleteDirectoryCommand(const QString& rkText, CResourceStore *pStore, TString ParentPath, TString DirName) + : IUndoCommand(rkText) , mpStore(pStore) , mParentPath(ParentPath) , mDirName(DirName) @@ -64,7 +64,7 @@ class CCreateDirectoryCommand : public ICreateDeleteDirectoryCommand { public: CCreateDirectoryCommand(CResourceStore *pStore, TString ParentPath, TString DirName) - : ICreateDeleteDirectoryCommand(pStore, ParentPath, DirName) + : ICreateDeleteDirectoryCommand("Create Directory", pStore, ParentPath, DirName) {} void undo() { DoDelete(); } @@ -75,7 +75,7 @@ class CDeleteDirectoryCommand : public ICreateDeleteDirectoryCommand { public: CDeleteDirectoryCommand(CResourceStore *pStore, TString ParentPath, TString DirName) - : ICreateDeleteDirectoryCommand(pStore, ParentPath, DirName) + : ICreateDeleteDirectoryCommand("Delete Directory", pStore, ParentPath, DirName) { mpDir = pStore->GetVirtualDirectory(ParentPath + DirName, false); ASSERT(mpDir); diff --git a/src/Editor/Undo/ICreateDeleteResourceCommand.h b/src/Editor/Undo/ICreateDeleteResourceCommand.h new file mode 100644 index 00000000..fff6c63e --- /dev/null +++ b/src/Editor/Undo/ICreateDeleteResourceCommand.h @@ -0,0 +1,70 @@ +#ifndef ICREATEDELETERESOURCECOMMAND_H +#define ICREATEDELETERESOURCECOMMAND_H + +#include "IUndoCommand.h" +#include "Editor/CEditorApplication.h" +#include "Editor/ResourceBrowser/CResourceBrowser.h" +#include +#include + +class ICreateDeleteResourceCommand : public IUndoCommand +{ +protected: + CResourceEntry* mpEntry; + TString mDirPath; + +public: + ICreateDeleteResourceCommand(const QString& kText, CResourceEntry* pEntry) + : IUndoCommand(kText) + , mpEntry(pEntry) + { + mDirPath = mpEntry->Directory()->FullPath(); + } + + void DoCreate() + { + CVirtualDirectory* pDir = mpEntry->ResourceStore()->GetVirtualDirectory(mDirPath, true); + gpEdApp->ResourceBrowser()->ResourceAboutToBeCreated(pDir); + + // restore directory and undelete + mpEntry->MarkDeleted(false); + + gpEdApp->ResourceBrowser()->ResourceCreated(mpEntry); + } + + void DoDelete() + { + gpEdApp->ResourceBrowser()->ResourceAboutToBeDeleted(mpEntry); + + // save directory and delete + mpEntry->MarkDeleted(true); + + gpEdApp->ResourceBrowser()->ResourceDeleted(); + } + + bool AffectsCleanState() const { return false; } +}; + +class CCreateResourceCommand : public ICreateDeleteResourceCommand +{ +public: + CCreateResourceCommand(CResourceEntry* pEntry) + : ICreateDeleteResourceCommand("Create Resource", pEntry) + {} + + void undo() { DoDelete(); } + void redo() { DoCreate(); } +}; + +class CDeleteResourceCommand : public ICreateDeleteResourceCommand +{ +public: + CDeleteResourceCommand(CResourceEntry* pEntry) + : ICreateDeleteResourceCommand("Delete Resource", pEntry) + {} + + void undo() { DoCreate(); } + void redo() { DoDelete(); } +}; + +#endif // ICREATEDELETERESOURCECOMMAND_H diff --git a/src/Editor/WorldEditor/WModifyTab.cpp b/src/Editor/WorldEditor/WModifyTab.cpp index b8b6e69d..a4154353 100644 --- a/src/Editor/WorldEditor/WModifyTab.cpp +++ b/src/Editor/WorldEditor/WModifyTab.cpp @@ -16,7 +16,6 @@ WModifyTab::WModifyTab(CWorldEditor *pEditor, QWidget *pParent) , mIsPicking(false) { ui->setupUi(this); - ui->PropertyView->InitColumnWidths(0.3f, 0.3f); ui->PropertyView->SetEditor(pEditor); mpWorldEditor = pEditor; diff --git a/templates/PropertyMap.xml b/templates/PropertyMap.xml index b5ebe213..ded01c19 100644 --- a/templates/PropertyMap.xml +++ b/templates/PropertyMap.xml @@ -36795,7 +36795,7 @@ - +