Added ability to create brand new SCAN/STRG assets. Added ability to update old projects.

This commit is contained in:
Aruki 2019-02-02 17:32:19 -07:00
parent 1e997dac46
commit 56843e214d
33 changed files with 523 additions and 175 deletions

2
externals/LibCommon vendored

@ -1 +1 @@
Subproject commit c98f4b3dcef68724627af91ac4103de7f28d2a2b
Subproject commit 5c3bfbe57f6ef8a300933afdc053a445cabd5c7c

View File

@ -626,7 +626,7 @@ void CGameExporter::ExportResource(SResourceInstance& rRes)
Name = rRes.ResourceID.ToString();
#endif
CResourceEntry *pEntry = mpStore->RegisterResource(rRes.ResourceID, CResTypeInfo::TypeForCookedExtension(mGame, rRes.ResourceType)->Type(), Directory, Name);
CResourceEntry *pEntry = mpStore->CreateNewResource(rRes.ResourceID, CResTypeInfo::TypeForCookedExtension(mGame, rRes.ResourceType)->Type(), Directory, Name);
// Set flags
pEntry->SetFlag(EResEntryFlag::IsBaseGameResource);

View File

@ -1,4 +1,5 @@
#include "CGameProject.h"
#include "CResourceIterator.h"
#include "IUIRelay.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include <Common/Serialization/XML.h>
@ -233,7 +234,11 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
pProj->mpResourceStore = std::make_unique<CResourceStore>(pProj);
LoadSuccess = pProj->mpResourceStore->LoadDatabaseCache();
// Validate resource database
// Removed database validation step. We used to do this on project load to make sure all data was correct, but this takes a long
// time and significantly extends how long it takes to open a project. In actual practice, this isn't needed most of the time, and
// in the odd case that it is needed, there is a button in the resource browser to rebuild the database. So in the interest of
// making project startup faster, we no longer validate the database.
#if 0
if (LoadSuccess)
{
pProgress->Report("Validating resource database");
@ -253,6 +258,7 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
LoadSuccess = false;
}
}
#endif
}
if (!LoadSuccess)
@ -263,6 +269,34 @@ CGameProject* CGameProject::LoadProject(const TString& rkProjPath, IProgressNoti
pProj->mProjFileLock.Lock(ProjPath);
pProj->mpGameInfo->LoadGameInfo(pProj->mGame);
// Perform update
if (Reader.FileVersion() < (uint16) EProjectVersion::Current)
{
pProgress->Report("Updating project");
CResourceStore* pOldStore = gpResourceStore;
gpResourceStore = pProj->mpResourceStore.get();
for (CResourceIterator It; It; ++It)
{
if (It->TypeInfo()->CanBeSerialized() && !It->HasRawVersion())
{
It->Save(true, false);
// Touch the cooked file to update its last modified time.
// This prevents PWE from erroneously thinking the cooked file is outdated
// (due to the raw file we just made having a more recent last modified time)
FileUtil::UpdateLastModifiedTime( It->CookedAssetPath() );
}
}
pProj->mpResourceStore->ConditionalSaveStore();
pProj->Save();
gpResourceStore = pOldStore;
}
pProj->mpAudioManager->LoadAssets();
pProj->mpTweakManager->LoadTweaks();
return pProj;

View File

@ -19,6 +19,7 @@ namespace nod { class DiscWii; }
enum class EProjectVersion
{
Initial,
RawStrings,
// Add new versions before this line
Max,

View File

@ -59,6 +59,16 @@ void CPackage::UpdateDependencyCache() const
mCacheDirty = false;
}
void CPackage::MarkDirty()
{
if (!mNeedsRecook)
{
mNeedsRecook = true;
Save();
UpdateDependencyCache();
}
}
void CPackage::Cook(IProgressNotifier *pProgress)
{
SCOPED_TIMER(CookPackage);

View File

@ -60,6 +60,7 @@ public:
void Serialize(IArchive& rArc);
void AddResource(const TString& rkName, const CAssetID& rkID, const CFourCC& rkType);
void UpdateDependencyCache() const;
void MarkDirty();
void Cook(IProgressNotifier *pProgress);
void CompareOriginalAssetList(const std::list<CAssetID>& rkNewList);
@ -77,7 +78,6 @@ public:
inline bool NeedsRecook() const { return mNeedsRecook; }
inline void SetPakName(TString NewName) { mPakName = NewName; }
inline void MarkDirty() { mNeedsRecook = true; Save(); UpdateDependencyCache(); }
};
#endif // CPACKAGE

View File

@ -40,6 +40,15 @@ CResourceEntry* CResourceEntry::CreateNewResource(CResourceStore *pStore, const
pEntry->mpDirectory->AddChild("", pEntry);
pEntry->mMetadataDirty = true;
// Check if the data exists or not. If so, then we are creating an entry for an existing resource (game exporter).
// If not, we want to initiate the new resource data and save it as soon as possible.
if (!pEntry->HasCookedVersion())
{
pEntry->mpResource = CResourceFactory::SpawnResource(pEntry);
pEntry->mpResource->InitializeNewResource();
}
return pEntry;
}
@ -261,7 +270,7 @@ bool CResourceEntry::NeedsRecook() const
return (FileUtil::LastModifiedTime(CookedAssetPath()) < FileUtil::LastModifiedTime(RawAssetPath()));
}
bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
bool CResourceEntry::Save(bool SkipCacheSave /*= false*/, bool FlagForRecook /*= true*/)
{
// SkipCacheSave argument tells us not to save the resource cache file. This is generally not advised because we don't
// want the actual resource data to desync from the cache data. However, there are occasions where we save multiple
@ -270,7 +279,6 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
//
// For now, always save the resource when this function is called even if there's been no changes made to it in memory.
// In the future this might not be desired behavior 100% of the time.
// We also might want this function to trigger a cook for certain resource types eventually.
bool ShouldCollectGarbage = false;
// Save raw resource
@ -298,7 +306,10 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
return false;
}
SetFlag(EResEntryFlag::NeedsRecook);
if (FlagForRecook)
{
SetFlag(EResEntryFlag::NeedsRecook);
}
}
// This resource type doesn't have a raw format; save cooked instead
@ -324,12 +335,15 @@ bool CResourceEntry::Save(bool SkipCacheSave /*= false*/)
}
// Flag dirty any packages that contain this resource.
for (uint32 iPkg = 0; iPkg < mpStore->Project()->NumPackages(); iPkg++)
if (FlagForRecook)
{
CPackage *pPkg = mpStore->Project()->PackageByIndex(iPkg);
for (uint32 iPkg = 0; iPkg < mpStore->Project()->NumPackages(); iPkg++)
{
CPackage *pPkg = mpStore->Project()->PackageByIndex(iPkg);
if (pPkg->ContainsAsset(ID()))
pPkg->MarkDirty();
if (!pPkg->NeedsRecook() && pPkg->ContainsAsset(ID()))
pPkg->MarkDirty();
}
}
if (ShouldCollectGarbage)

View File

@ -67,7 +67,7 @@ public:
bool IsInDirectory(CVirtualDirectory *pDir) const;
uint64 Size() const;
bool NeedsRecook() const;
bool Save(bool SkipCacheSave = false);
bool Save(bool SkipCacheSave = false, bool FlagForRecook = true);
bool Cook();
CResource* Load();
CResource* LoadCooked(IInputStream& rInput);
@ -87,7 +87,7 @@ public:
inline void SetFlagEnabled(EResEntryFlag Flag, bool Enabled) { Enabled ? SetFlag(Flag) : ClearFlag(Flag); }
inline void SetDirty() { SetFlag(EResEntryFlag::NeedsRecook); }
inline void SetHidden(bool Hidden) { Hidden ? SetFlag(EResEntryFlag::Hidden) : ClearFlag(EResEntryFlag::Hidden); }
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); }

View File

@ -284,7 +284,14 @@ void CResourceStore::ClearDatabase()
{
// THIS OPERATION REQUIRES THAT ALL RESOURCES ARE UNREFERENCED
DestroyUnreferencedResources();
ASSERT(mLoadedResources.empty());
if (!mLoadedResources.empty())
{
debugf("ERROR: Resources still loaded:");
for (auto Iter = mLoadedResources.begin(); Iter != mLoadedResources.end(); Iter++)
debugf("\t[%s] %s", *Iter->first.ToString(), *Iter->second->CookedAssetPath(true));
ASSERT(false);
}
// Clear out existing resource entries and directories
for (auto Iter = mResourceEntries.begin(); Iter != mResourceEntries.end(); Iter++)
@ -384,7 +391,7 @@ bool CResourceStore::IsResourceRegistered(const CAssetID& rkID) const
return FindEntry(rkID) != nullptr;
}
CResourceEntry* CResourceStore::RegisterResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName)
CResourceEntry* CResourceStore::CreateNewResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName)
{
CResourceEntry *pEntry = FindEntry(rkID);
@ -398,6 +405,12 @@ CResourceEntry* CResourceStore::RegisterResource(const CAssetID& rkID, EResource
{
pEntry = CResourceEntry::CreateNewResource(this, rkID, rkDir, rkName, Type);
mResourceEntries[rkID] = pEntry;
mDatabaseCacheDirty = true;
if (pEntry->IsLoaded())
{
TrackLoadedResource(pEntry);
}
}
else

View File

@ -54,7 +54,7 @@ public:
TString DefaultResourceDirPath() const;
bool IsResourceRegistered(const CAssetID& rkID) const;
CResourceEntry* RegisterResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName);
CResourceEntry* CreateNewResource(const CAssetID& rkID, EResourceType Type, const TString& rkDir, const TString& rkName);
CResourceEntry* FindEntry(const CAssetID& rkID) const;
CResourceEntry* FindEntry(const TString& rkPath) const;
bool AreAllEntriesValid() const;

View File

@ -92,7 +92,7 @@ void CAnimationParameters::Write(IOutputStream& rSCLY)
{
CAssetID::skInvalidID32.Write(rSCLY);
rSCLY.WriteLong(0);
rSCLY.WriteLong(0);
rSCLY.WriteLong(mGame >= EGame::EchoesDemo ? 0 : 0xFFFFFFFF);
}
}

View File

@ -10,6 +10,7 @@ CResTypeInfo::CResTypeInfo(EResourceType Type, const TString& rkTypeName, const
, mRetroExtension(rkRetroExtension)
, mCanBeSerialized(false)
, mCanHaveDependencies(true)
, mCanBeCreated(false)
{
#if !PUBLIC_RELEASE
ASSERT(smTypeMap.find(Type) == smTypeMap.end());
@ -332,6 +333,7 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Scan, "Scan", "scan");
AddExtension(pType, "SCAN", EGame::PrimeDemo, EGame::Corruption);
pType->mCanBeCreated = true;
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Skeleton, "Skeleton", "cin");
@ -382,6 +384,7 @@ void CResTypeInfo::CResTypeInfoFactory::InitTypes()
CResTypeInfo *pType = new CResTypeInfo(EResourceType::StringTable, "String Table", "strg");
AddExtension(pType, "STRG", EGame::PrimeDemo, EGame::DKCReturns);
pType->mCanBeSerialized = true;
pType->mCanBeCreated = true;
}
{
CResTypeInfo *pType = new CResTypeInfo(EResourceType::Texture, "Texture", "txtr");

View File

@ -23,6 +23,7 @@ class CResTypeInfo
TString mRetroExtension; // File extension in Retro's directory tree. We don't use it directly but it is needed for generating asset ID hashes
bool mCanBeSerialized;
bool mCanHaveDependencies;
bool mCanBeCreated;
static std::unordered_map<EResourceType, CResTypeInfo*> smTypeMap;
@ -36,10 +37,11 @@ public:
CFourCC CookedExtension(EGame Game) const;
// Accessors
inline EResourceType Type() const { return mType; }
inline EResourceType Type() const { return mType; }
inline TString TypeName() const { return mTypeName; }
inline bool CanBeSerialized() const { return mCanBeSerialized; }
inline bool CanHaveDependencies() const { return mCanHaveDependencies; }
inline bool CanBeCreated() const { return mCanBeCreated; }
// Static
static void GetAllTypesInGame(EGame Game, std::list<CResTypeInfo*>& rOut);

View File

@ -43,6 +43,7 @@ public:
virtual ~CResource() {}
virtual CDependencyTree* BuildDependencyTree() const { return new CDependencyTree(); }
virtual void Serialize(IArchive& /*rArc*/) {}
virtual void InitializeNewResource() {}
inline CResourceEntry* Entry() const { return mpEntry; }
inline CResTypeInfo* TypeInfo() const { return mpEntry->TypeInfo(); }

View File

@ -1,4 +1,5 @@
#include "CStringTable.h"
#include "Core/GameProject/CGameProject.h"
#include <Common/Math/MathUtil.h>
#include <algorithm>
#include <iterator>
@ -8,27 +9,27 @@
* This is also the order that languages appear in game STRG assets.
*/
// Supported languages in the original NTSC release of Metroid Prime
const ELanguage gkSupportedLanguagesMP1[] =
const std::vector<ELanguage> gkSupportedLanguagesMP1 =
{
ELanguage::English
};
// Supported languages in the PAL version of Metroid Prime, and also Metroid Prime 2
const ELanguage gkSupportedLanguagesMP1PAL[] =
const std::vector<ELanguage> gkSupportedLanguagesMP1PAL =
{
ELanguage::English, ELanguage::French, ELanguage::German,
ELanguage::Spanish, ELanguage::Italian, ELanguage::Japanese
};
// Supported languages in Metroid Prime 3
const ELanguage gkSupportedLanguagesMP3[] =
const std::vector<ELanguage> gkSupportedLanguagesMP3 =
{
ELanguage::English, ELanguage::Japanese, ELanguage::German,
ELanguage::French, ELanguage::Spanish, ELanguage::Italian
};
// Supported languages in DKCR
const ELanguage gkSupportedLanguagesDKCR[] =
const std::vector<ELanguage> gkSupportedLanguagesDKCR =
{
ELanguage::English, ELanguage::Japanese, ELanguage::German,
ELanguage::French, ELanguage::Spanish, ELanguage::Italian,
@ -36,6 +37,32 @@ const ELanguage gkSupportedLanguagesDKCR[] =
ELanguage::NAFrench, ELanguage::NASpanish
};
// Utility function - retrieve the language array for a given game/region
static const std::vector<ELanguage>& GetSupportedLanguages(EGame Game, ERegion Region)
{
switch (Game)
{
default:
case EGame::PrimeDemo:
case EGame::Prime:
if (Region == ERegion::NTSC)
return gkSupportedLanguagesMP1;
else
return gkSupportedLanguagesMP1PAL;
case EGame::EchoesDemo:
case EGame::Echoes:
case EGame::CorruptionProto:
return gkSupportedLanguagesMP1PAL;
case EGame::Corruption:
return gkSupportedLanguagesMP3;
case EGame::DKCReturns:
return gkSupportedLanguagesDKCR;
}
}
// Utility function - retrieve the index of a given language
static int FindLanguageIndex(const CStringTable* pkInTable, ELanguage InLanguage)
{
@ -192,10 +219,19 @@ void CStringTable::RemoveString(uint StringIndex)
}
}
/** Configures the string table with default languages for the game/region pairing of the resource */
void CStringTable::ConfigureDefaultLanguages()
/** Initialize new resource data */
void CStringTable::InitializeNewResource()
{
//@todo; this should be called on all newly created string tables
// Initialize data for whatever languages are supported by our game/region
ERegion Region = ( Entry() && Entry()->Project() ? Entry()->Project()->Region() : ERegion::NTSC );
const std::vector<ELanguage>& kLanguageArray = GetSupportedLanguages(Game(), Region);
mLanguages.resize( kLanguageArray.size() );
for (uint i=0; i < kLanguageArray.size(); i++)
{
mLanguages[i].Language = kLanguageArray[i];
mLanguages[i].Strings.resize(1);
}
}
/** Serialize resource data */
@ -344,36 +380,12 @@ TString CStringTable::StripFormatting(const TString& kInString)
/** Static - Returns whether a given language is supported by the given game/region combination */
bool CStringTable::IsLanguageSupported(ELanguage Language, EGame Game, ERegion Region)
{
// Pick the correct array to iterate based on which game/region was requested.
const ELanguage* pkSupportedLanguages = nullptr;
uint NumLanguages = 0;
if (Game <= EGame::Prime && Region == ERegion::NTSC)
{
return (Language == ELanguage::English);
}
else if (Game <= EGame::CorruptionProto)
{
pkSupportedLanguages = &gkSupportedLanguagesMP1PAL[0];
NumLanguages = ARRAY_SIZE( gkSupportedLanguagesMP1PAL );
}
else if (Game <= EGame::Corruption)
{
pkSupportedLanguages = &gkSupportedLanguagesMP3[0];
NumLanguages = ARRAY_SIZE( gkSupportedLanguagesMP3 );
}
else if (Game <= EGame::DKCReturns)
{
pkSupportedLanguages = &gkSupportedLanguagesDKCR[0];
NumLanguages = ARRAY_SIZE( gkSupportedLanguagesDKCR );
}
ASSERT(pkSupportedLanguages);
ASSERT(NumLanguages > 0);
const std::vector<ELanguage>& kLanguageArray = GetSupportedLanguages(Game, Region);
// Check if the requested language is in the array.
for (uint LanguageIdx = 0; LanguageIdx < NumLanguages; LanguageIdx++)
for (uint LanguageIdx = 0; LanguageIdx < kLanguageArray.size(); LanguageIdx++)
{
if (pkSupportedLanguages[LanguageIdx] == Language)
if (kLanguageArray[LanguageIdx] == Language)
{
return true;
}

View File

@ -84,14 +84,14 @@ public:
/** Remove a string from the table */
void RemoveString(uint StringIndex);
/** Configures the string table with default languages for the game/region pairing of the resource */
void ConfigureDefaultLanguages();
/** Initialize new resource data */
virtual void InitializeNewResource() override;
/** Serialize resource data */
virtual void Serialize(IArchive& Arc);
virtual void Serialize(IArchive& Arc) override;
/** Build the dependency tree for this resource */
virtual CDependencyTree* BuildDependencyTree() const;
virtual CDependencyTree* BuildDependencyTree() const override;
/** Static - Strip all formatting tags for a given string */
static TString StripFormatting(const TString& kInString);

View File

@ -9,6 +9,11 @@ CTweakManager::CTweakManager(CGameProject* pInProject)
{
}
CTweakManager::~CTweakManager()
{
ClearTweaks();
}
void CTweakManager::LoadTweaks()
{
ASSERT( mTweakObjects.empty() );
@ -33,21 +38,6 @@ void CTweakManager::LoadTweaks()
}
}
CTweakManager::~CTweakManager()
{
for (CTweakData* pTweakData : mTweakObjects)
{
if (pTweakData->Entry() != nullptr)
{
pTweakData->Release();
}
else
{
delete pTweakData;
}
}
}
bool CTweakManager::SaveTweaks()
{
// MP1 - Save all tweak assets
@ -82,3 +72,19 @@ bool CTweakManager::SaveTweaks()
return CTweakCooker::CookNTWK(mTweakObjects, StandardNTWK);
}
}
void CTweakManager::ClearTweaks()
{
for (CTweakData* pTweakData : mTweakObjects)
{
if (pTweakData->Entry() != nullptr)
{
pTweakData->Release();
}
else
{
delete pTweakData;
}
}
mTweakObjects.clear();
}

View File

@ -17,6 +17,7 @@ public:
~CTweakManager();
void LoadTweaks();
bool SaveTweaks();
void ClearTweaks();
// Accessors
inline const std::vector<CTweakData*>& TweakObjects() const

View File

@ -249,6 +249,7 @@ bool CEditorApplication::RebuildResourceDatabase()
{
// Fake-close the project, but keep it in memory so we can modify the resource store
CGameProject *pProj = mpActiveProject;
mpActiveProject->TweakManager()->ClearTweaks();
mpActiveProject = nullptr;
emit ActiveProjectChanged(nullptr);
@ -263,6 +264,7 @@ bool CEditorApplication::RebuildResourceDatabase()
// Set project to active again
mpActiveProject = pProj;
mpActiveProject->TweakManager()->LoadTweaks();
emit ActiveProjectChanged(pProj);
UICommon::InfoMsg(mpWorldEditor, "Success", "Resource database rebuilt successfully!");

View File

@ -34,7 +34,12 @@ CPropertyDelegate::CPropertyDelegate(QObject* pParent /*= 0*/)
, mEditInProgress(false)
, mRelaysBlocked(false)
{
mpEditor = UICommon::FindAncestor<IEditor>(this);
mpEditor = UICommon::FindAncestor<IEditor>(pParent);
}
void CPropertyDelegate::SetEditor(IEditor* pEditor)
{
mpEditor = pEditor;
}
void CPropertyDelegate::SetPropertyModel(CPropertyModel* pModel)

View File

@ -83,6 +83,11 @@ bool CPropertyView::event(QEvent *pEvent)
else return QTreeView::event(pEvent);
}
void CPropertyView::SetEditor(IEditor* pEditor)
{
mpDelegate->SetEditor(pEditor);
}
void CPropertyView::InitColumnWidths(float NameColumnPercentage, float ValueColumnPercentage)
{
header()->resizeSection(0, width() * NameColumnPercentage);

View File

@ -25,6 +25,7 @@ public:
CPropertyView(QWidget* pParent = 0);
void setModel(QAbstractItemModel* pModel);
bool event(QEvent* pEvent);
void SetEditor(IEditor* pEditor);
void InitColumnWidths(float NameColumnPercentage, float ValueColumnPercentage);
void ClearProperties();
void SetIntrinsicProperties(CStructRef InProperties);

View File

@ -28,6 +28,7 @@ CResourceBrowser::CResourceBrowser(QWidget *pParent)
, mEditorStore(false)
, mAssetListMode(false)
, mSearching(false)
, mpAddMenu(nullptr)
, mpInspectedEntry(nullptr)
{
mpUI->setupUi(this);
@ -144,13 +145,12 @@ CResourceBrowser::CResourceBrowser(QWidget *pParent)
// 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->NewFolderButton, SIGNAL(pressed()), this, SLOT(CreateDirectory()));
connect(mpUI->ClearButton, SIGNAL(pressed()), this, SLOT(OnClearButtonPressed()));
connect(mpUI->DirectoryTreeView, SIGNAL(clicked(QModelIndex)), this, SLOT(OnDirectorySelectionChanged(QModelIndex)));
@ -281,6 +281,50 @@ void CResourceBrowser::CreateFilterCheckboxes()
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->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)
@ -400,6 +444,49 @@ bool CResourceBrowser::MoveResources(const QList<CResourceEntry*>& rkResources,
return true;
}
CResourceEntry* CResourceBrowser::CreateNewResource(EResourceType Type)
{
// Create new asset ID. Sanity check to make sure the ID is unused.
CAssetID NewAssetID;
while (!NewAssetID.IsValid() || mpStore->FindEntry(NewAssetID) != nullptr)
{
NewAssetID = CAssetID::RandomID( mpStore->Game() );
}
// Boring generic default name - user will immediately be prompted to change this
TString BaseName = TString::Format(
"New %s", *CResTypeInfo::FindTypeInfo(Type)->TypeName()
);
TString Name = BaseName;
int Num = 0;
while (mpSelectedDir->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();
emit ResourceCreated(pEntry);
// 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)
@ -498,6 +585,23 @@ void CResourceBrowser::OnSortModeChanged(int Index)
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)
@ -685,7 +789,8 @@ void CResourceBrowser::UpdateStore()
mpUI->SearchBar->clear();
mSearching = false;
// Refresh type filter list
// Refresh project-specific UI
CreateAddMenu();
CreateFilterCheckboxes();
// Refresh directory tree

View File

@ -5,7 +5,9 @@
#include "CResourceProxyModel.h"
#include "CResourceTableModel.h"
#include "CVirtualDirectoryModel.h"
#include <QCheckBox>
#include <QMenu>
#include <QTimer>
#include <QUndoStack>
#include <QVBoxLayout>
@ -29,6 +31,9 @@ class CResourceBrowser : public QWidget
bool mAssetListMode;
bool mSearching;
// Add Menu
QMenu *mpAddMenu;
// Type Filter
QWidget *mpFilterBoxesContainerWidget;
QVBoxLayout *mpFilterBoxesLayout;
@ -59,11 +64,16 @@ public:
void SelectResource(CResourceEntry *pEntry, bool ClearFiltersIfNecessary = false);
void SelectDirectory(CVirtualDirectory *pDir);
void CreateFilterCheckboxes();
void CreateAddMenu();
void AddCreateAssetMenuActions(QMenu* pMenu);
bool RenameResource(CResourceEntry *pEntry, const TString& rkNewName);
bool RenameDirectory(CVirtualDirectory *pDir, const TString& rkNewName);
bool MoveResources(const QList<CResourceEntry*>& rkResources, const QList<CVirtualDirectory*>& rkDirectories, CVirtualDirectory *pNewDir);
CResourceEntry* CreateNewResource(EResourceType Type);
// Interface
bool eventFilter(QObject *pWatched, QEvent *pEvent);
@ -82,6 +92,7 @@ public slots:
void SetResourceListView();
void OnClearButtonPressed();
void OnSortModeChanged(int Index);
void OnCreateAssetAction();
bool CreateDirectory();
bool DeleteDirectories(const QList<CVirtualDirectory*>& rkDirs);
void OnSearchStringChanged(QString SearchString);
@ -116,6 +127,12 @@ signals:
void ResourceAboutToBeMoved(CResourceEntry *pRes, QString NewPath);
void ResourceMoved(CResourceEntry *pRes, CVirtualDirectory *pOldDir, TString OldName);
void ResourceAboutToBeCreated(CVirtualDirectory* pInDir);
void ResourceCreated(CResourceEntry *pRes);
void ResourceAboutToBeDeleted(CResourceEntry *pRes);
void ResourceDeleted();
void DirectoryAboutToBeMoved(CVirtualDirectory *pDir, QString NewPath);
void DirectoryMoved(CVirtualDirectory *pDir, CVirtualDirectory *pOldDir, TString OldName);

View File

@ -141,9 +141,18 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="NewFolderButton">
<widget class="QPushButton" name="AddButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string> New Folder</string>
<string/>
</property>
<property name="icon">
<iconset resource="../Icons.qrc">
@ -151,8 +160,8 @@
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>

View File

@ -9,180 +9,252 @@ CResourceTableContextMenu::CResourceTableContextMenu(CResourceBrowser *pBrowser,
, mpTable(pView)
, mpModel(pModel)
, mpProxy(pProxy)
, mpEntry(nullptr)
, mpDirectory(nullptr)
, mpClickedEntry(nullptr)
, mpClickedDirectory(nullptr)
{
// Connect to the view
connect(pView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(ShowMenu(QPoint)));
}
// Create actions
#if WIN32
QString OpenInExplorerString = "Show in Explorer";
#elif __APPLE__
QString OpenInExplorerString = "Show in Finder";
#else
QString OpenInExplorerString = "Show in file manager";
#endif
void CResourceTableContextMenu::InitMenu()
{
// Clear existing menu items
clear();
if (mpClickedEntry)
{
#if WIN32
const QString kOpenInExplorerString = "Show in Explorer";
#elif __APPLE__
const QString kOpenInExplorerString = "Show in Finder";
#else
const QString kOpenInExplorerString = "Show in file manager";
#endif
addAction("Open", this, SLOT(Open()));
addAction("Open in External Application", this, SLOT(OpenInExternalApp()));
addAction(kOpenInExplorerString, this, SLOT(OpenInExplorer()));
addSeparator();
}
if (mpClickedEntry || mpClickedDirectory)
{
addAction("Rename", this, SLOT(Rename()));
if (mpModel->IsDisplayingAssetList())
{
addAction("Select Folder", this, SLOT(SelectFolder()));
}
if (mpClickedEntry)
{
addAction("Show Referencers", this, SLOT(ShowReferencers()));
addAction("Show Dependencies", this, SLOT(ShowDependencies()));
}
}
if (mpClickedEntry || mpClickedDirectory || !mSelectedIndexes.isEmpty())
{
addAction("Delete", this, SLOT(Delete()));
}
mpOpenAction = addAction("Open", this, SLOT(Open()));
mpOpenInExternalAppAction = addAction("Open in External Application", this, SLOT(OpenInExternalApp()));
mpOpenInExplorerAction = addAction(OpenInExplorerString, this, SLOT(OpenInExplorer()));
addSeparator();
mpRenameAction = addAction("Rename", this, SLOT(Rename()));
mpSelectFolderAction = addAction("Select Folder", this, SLOT(SelectFolder()));
mpShowReferencersAction = addAction("Show Referencers", this, SLOT(ShowReferencers()));
mpShowDependenciesAction = addAction("Show Dependencies", this, SLOT(ShowDependencies()));
mpDeleteAction = addAction("Delete", this, SLOT(Delete()));
addSeparator();
mpCopyNameAction = addAction("Copy Name", this, SLOT(CopyName()));
mpCopyPathAction = addAction("Copy Path", this, SLOT(CopyPath()));
mpCopyIDAction = addAction("Copy Asset ID", this, SLOT(CopyID()));
if (mpClickedEntry)
{
addAction("Copy Name", this, SLOT(CopyName()));
addAction("Copy Path", this, SLOT(CopyPath()));
addAction("Copy ID", this, SLOT(CopyID()));
addSeparator();
}
QMenu* pCreate = addMenu("Create...");
mpBrowser->AddCreateAssetMenuActions(pCreate);
}
void CResourceTableContextMenu::ShowMenu(const QPoint& rkPos)
{
if (mpBrowser->CurrentStore() == nullptr)
return;
// Fetch the entry/directory
mProxyIndex = mpTable->indexAt(rkPos);
mClickedProxyIndex = mpTable->indexAt(rkPos);
if (mProxyIndex.isValid())
if (mClickedProxyIndex.isValid())
{
mIndex = mpProxy->mapToSource(mProxyIndex);
mpEntry = mpModel->IndexEntry(mIndex);
mpDirectory = mpModel->IndexDirectory(mIndex);
// Show/hide menu options
bool IsRes = (mpEntry != nullptr);
mpOpenInExternalAppAction->setVisible(IsRes);
mpSelectFolderAction->setVisible(mpModel->IsDisplayingAssetList());
mpShowDependenciesAction->setVisible(IsRes);
mpShowReferencersAction->setVisible(IsRes);
mpDeleteAction->setVisible(mpDirectory && mpDirectory->IsEmpty(true));
mpCopyIDAction->setVisible(IsRes);
// Exec menu
QPoint GlobalPos = mpTable->viewport()->mapToGlobal(rkPos);
exec(GlobalPos);
mClickedIndex = mpProxy->mapToSource(mClickedProxyIndex);
mpClickedEntry = mpModel->IndexEntry(mClickedIndex);
mpClickedDirectory = mpModel->IndexDirectory(mClickedIndex);
}
else
{
mClickedIndex = QModelIndex();
mpClickedEntry = nullptr;
mpClickedDirectory = nullptr;
}
// Fetch the list of selected indexes
QItemSelection Selection = mpProxy->mapSelectionToSource( mpTable->selectionModel()->selection() );
mSelectedIndexes = Selection.indexes();
InitMenu();
// Exec menu
QPoint GlobalPos = mpTable->viewport()->mapToGlobal(rkPos);
exec(GlobalPos);
}
// Menu Options
void CResourceTableContextMenu::Open()
{
if (mpEntry)
gpEdApp->EditResource(mpEntry);
if (mpClickedEntry)
gpEdApp->EditResource(mpClickedEntry);
else
mpBrowser->SetActiveDirectory(mpDirectory);
mpBrowser->SetActiveDirectory(mpClickedDirectory);
}
void CResourceTableContextMenu::OpenInExternalApp()
{
ASSERT(mpEntry);
UICommon::OpenInExternalApplication( TO_QSTRING(mpEntry->CookedAssetPath()) );
ASSERT(mpClickedEntry);
UICommon::OpenInExternalApplication( TO_QSTRING(mpClickedEntry->CookedAssetPath()) );
}
void CResourceTableContextMenu::OpenInExplorer()
{
if (mpEntry)
if (mpClickedEntry)
{
QString Path = TO_QSTRING( mpEntry->CookedAssetPath() );
QString Path = TO_QSTRING( mpClickedEntry->CookedAssetPath() );
UICommon::OpenContainingFolder(Path);
}
else
{
TString BasePath = mpBrowser->CurrentStore()->ResourcesDir();
QString Path = TO_QSTRING( BasePath + mpDirectory->FullPath() );
QString Path = TO_QSTRING( BasePath + mpClickedDirectory->FullPath() );
UICommon::OpenContainingFolder(Path);
}
}
void CResourceTableContextMenu::Rename()
{
mpTable->edit(mProxyIndex);
mpTable->edit(mClickedProxyIndex);
}
void CResourceTableContextMenu::SelectFolder()
{
CVirtualDirectory *pDir = (mpEntry ? mpEntry->Directory() : mpDirectory->Parent());
CVirtualDirectory *pDir = (mpClickedEntry ? mpClickedEntry->Directory() : mpClickedDirectory->Parent());
mpBrowser->SetActiveDirectory(pDir);
if (mpEntry)
mpBrowser->SelectResource(mpEntry);
if (mpClickedEntry)
mpBrowser->SelectResource(mpClickedEntry);
else
mpBrowser->SelectDirectory(mpDirectory);
mpBrowser->SelectDirectory(mpClickedDirectory);
}
void CResourceTableContextMenu::ShowReferencers()
{
ASSERT(mpEntry);
ASSERT(mpClickedEntry);
QList<CResourceEntry*> EntryList;
for (CResourceIterator Iter(mpEntry->ResourceStore()); Iter; ++Iter)
for (CResourceIterator Iter(mpClickedEntry->ResourceStore()); Iter; ++Iter)
{
if (Iter->Dependencies()->HasDependency(mpEntry->ID()))
if (Iter->Dependencies()->HasDependency(mpClickedEntry->ID()))
EntryList << *Iter;
}
if (!mpModel->IsDisplayingUserEntryList())
mpBrowser->SetInspectedEntry(mpEntry);
mpBrowser->SetInspectedEntry(mpClickedEntry);
QString ListDesc = QString("Referencers of \"%1\"").arg( TO_QSTRING(mpEntry->CookedAssetPath().GetFileName()) );
QString ListDesc = QString("Referencers of \"%1\"").arg( TO_QSTRING(mpClickedEntry->CookedAssetPath().GetFileName()) );
mpModel->DisplayEntryList(EntryList, ListDesc);
mpBrowser->ClearFilters();
}
void CResourceTableContextMenu::ShowDependencies()
{
ASSERT(mpEntry);
ASSERT(mpClickedEntry);
std::set<CAssetID> Dependencies;
mpEntry->Dependencies()->GetAllResourceReferences(Dependencies);
mpClickedEntry->Dependencies()->GetAllResourceReferences(Dependencies);
QList<CResourceEntry*> EntryList;
for (auto Iter = Dependencies.begin(); Iter != Dependencies.end(); Iter++)
{
CResourceEntry *pEntry = mpEntry->ResourceStore()->FindEntry(*Iter);
CResourceEntry *pEntry = mpClickedEntry->ResourceStore()->FindEntry(*Iter);
if (pEntry)
EntryList << pEntry;
}
if (!mpModel->IsDisplayingUserEntryList())
mpBrowser->SetInspectedEntry(mpEntry);
mpBrowser->SetInspectedEntry(mpClickedEntry);
QString ListDesc = QString("Dependencies of \"%1\"").arg( TO_QSTRING(mpEntry->CookedAssetPath().GetFileName()) );
QString ListDesc = QString("Dependencies of \"%1\"").arg( TO_QSTRING(mpClickedEntry->CookedAssetPath().GetFileName()) );
mpModel->DisplayEntryList(EntryList, ListDesc);
mpBrowser->ClearFilters();
}
void CResourceTableContextMenu::Delete()
{
ASSERT(mpDirectory && mpDirectory->IsEmpty(true));
// Create confirmation message
uint NumResources = 0, NumDirectories = 0;
QList<CVirtualDirectory*> List;
List << mpDirectory;
mpBrowser->DeleteDirectories(List);
foreach (const QModelIndex& kIndex, mSelectedIndexes)
{
if (mpModel->IsIndexDirectory(kIndex))
NumDirectories++;
else
NumResources++;
}
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<CVirtualDirectory*> List;
List << mpClickedDirectory;
mpBrowser->DeleteDirectories(List);
}
}
void CResourceTableContextMenu::CopyName()
{
if (mpEntry)
gpEdApp->clipboard()->setText( TO_QSTRING(mpEntry->Name()) );
if (mpClickedEntry)
gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedEntry->Name()) );
else
gpEdApp->clipboard()->setText( TO_QSTRING(mpDirectory->Name()) );
gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedDirectory->Name()) );
}
void CResourceTableContextMenu::CopyPath()
{
if (mpEntry)
gpEdApp->clipboard()->setText( TO_QSTRING(mpEntry->CookedAssetPath(true)) );
if (mpClickedEntry)
gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedEntry->CookedAssetPath(true)) );
else
gpEdApp->clipboard()->setText( TO_QSTRING(mpDirectory->FullPath()) );
gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedDirectory->FullPath()) );
}
void CResourceTableContextMenu::CopyID()
{
ASSERT(mpEntry);
gpEdApp->clipboard()->setText( TO_QSTRING(mpEntry->ID().ToString()) );
ASSERT(mpClickedEntry);
gpEdApp->clipboard()->setText( TO_QSTRING(mpClickedEntry->ID().ToString()) );
}

View File

@ -16,30 +16,17 @@ class CResourceTableContextMenu : public QMenu
CResourceTableModel *mpModel;
CResourceProxyModel *mpProxy;
QModelIndex mProxyIndex;
QModelIndex mIndex;
CResourceEntry *mpEntry;
CVirtualDirectory *mpDirectory;
// Actions
QAction *mpOpenAction;
QAction *mpOpenInExternalAppAction;
QAction *mpOpenInExplorerAction;
QAction *mpSelectFolderAction;
QAction *mpRenameAction;
QAction *mpShowReferencersAction;
QAction *mpShowDependenciesAction;
QAction *mpDeleteAction;
QAction *mpCopyNameAction;
QAction *mpCopyPathAction;
QAction *mpCopyIDAction;
QModelIndexList mSelectedIndexes;
QModelIndex mClickedIndex;
QModelIndex mClickedProxyIndex;
CResourceEntry *mpClickedEntry;
CVirtualDirectory *mpClickedDirectory;
public:
CResourceTableContextMenu(CResourceBrowser *pBrowser, QTableView *pView, CResourceTableModel *pModel, CResourceProxyModel *pProxy);
public slots:
void InitMenu();
void ShowMenu(const QPoint& rkPos);
// Menu Options

View File

@ -7,6 +7,7 @@ CResourceTableModel::CResourceTableModel(CResourceBrowser *pBrowser, QObject *pP
, mpCurrentDir(nullptr)
, mIsDisplayingUserEntryList(false)
{
connect(pBrowser, SIGNAL(ResourceCreated(CResourceEntry*)), this, SLOT(CheckAddResource(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)));
@ -155,7 +156,7 @@ Qt::DropActions CResourceTableModel::supportedDropActions() const
// ************ FUNCTIONALITY ************
QModelIndex CResourceTableModel::GetIndexForEntry(CResourceEntry *pEntry) const
{
auto Iter = qBinaryFind(mEntries, pEntry);
auto Iter = std::find(mEntries.begin(), mEntries.end(), pEntry);
if (Iter == mEntries.end())
return QModelIndex();
@ -289,6 +290,32 @@ void CResourceTableModel::RefreshAllIndices()
}
}
void CResourceTableModel::CheckAddResource(CResourceEntry *pEntry)
{
if ( (mIsAssetListMode && pEntry->IsInDirectory(mpCurrentDir)) ||
(!mIsAssetListMode && pEntry->Directory() == mpCurrentDir) )
{
// Append to the end, let the proxy handle sorting
int NumRows = mDirectories.size() + mEntries.size();
beginInsertRows(QModelIndex(), NumRows, NumRows);
mEntries << pEntry;
endInsertRows();
}
}
void CResourceTableModel::CheckRemoveResource(CResourceEntry *pEntry)
{
int Index = mEntries.indexOf(pEntry);
if (Index != -1)
{
Index += mDirectories.size();
beginRemoveRows(QModelIndex(), Index, Index);
mEntries.removeAt(Index);
endRemoveRows();
}
}
void CResourceTableModel::CheckAddDirectory(CVirtualDirectory *pDir)
{
if (pDir->Parent() == mpCurrentDir)

View File

@ -59,6 +59,8 @@ public:
public slots:
void RefreshAllIndices();
void CheckAddResource(CResourceEntry *pEntry);
void CheckRemoveResource(CResourceEntry *pEntry);
void CheckAddDirectory(CVirtualDirectory *pDir);
void CheckRemoveDirectory(CVirtualDirectory *pDir);
void OnResourceMoved(CResourceEntry *pEntry, CVirtualDirectory *pOldDir, TString OldName);

View File

@ -14,9 +14,24 @@ CScanEditor::CScanEditor(CScan* pScan, QWidget* pParent /*= 0*/)
QString WindowTitle = "%APP_FULL_NAME% - Scan Editor - %1[*]";
WindowTitle = WindowTitle.arg( TO_QSTRING(mpScan->Entry()->CookedAssetPath(true).GetFileName()) );
SET_WINDOWTITLE_APPVARS(WindowTitle);
connect( mpUI->ActionSave, SIGNAL(toggled(bool)), this, SLOT(Save()) );
connect( mpUI->ActionSaveAndCook, SIGNAL(toggled(bool)), this, SLOT(SaveAndRepack()) );
}
CScanEditor::~CScanEditor()
{
delete mpUI;
}
bool CScanEditor::Save()
{
if (mpScan->Entry()->Save())
{
UndoStack().setClean();
setWindowModified(false);
return true;
}
else
return false;
}

View File

@ -21,6 +21,9 @@ class CScanEditor : public IEditor
public:
explicit CScanEditor(CScan* pScan, QWidget* pParent = 0);
~CScanEditor();
public slots:
virtual bool Save() override;
};
#endif // CSCANEDITOR_H

View File

@ -46,7 +46,7 @@ CStringEditor::CStringEditor(CStringTable* pStringTable, QWidget* pParent)
, mpUI(new Ui::CStringEditor)
, mpStringTable(pStringTable)
, mCurrentLanguage(ELanguage::English)
, mCurrentStringIndex(0)
, mCurrentStringIndex(-1)
, mCurrentStringCount(0)
, mIsEditingStringName(false)
, mIsEditingStringData(false)

View File

@ -17,6 +17,7 @@ WModifyTab::WModifyTab(CWorldEditor *pEditor, QWidget *pParent)
{
ui->setupUi(this);
ui->PropertyView->InitColumnWidths(0.3f, 0.3f);
ui->PropertyView->SetEditor(pEditor);
mpWorldEditor = pEditor;