SourceAssetTreeModel.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "SourceAssetTreeModel.h"
  9. #include "SourceAssetTreeItemData.h"
  10. #include <AzCore/Component/TickBus.h>
  11. #include <AzCore/IO/Path/Path.h>
  12. #include <native/utilities/assetUtils.h>
  13. #include <native/utilities/IPathConversion.h>
  14. #include <AzCore/Console/IConsole.h>
  15. #include <AzCore/std/smart_ptr/make_shared.h>
  16. namespace AssetProcessor
  17. {
  18. AZ_CVAR(bool, ap_disableAssetTreeView, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Disable asset tree for automated tests.");
  19. SourceAssetTreeModel::SourceAssetTreeModel(AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> sharedDbConnection, QObject* parent) :
  20. AssetTreeModel(sharedDbConnection, parent)
  21. {
  22. }
  23. SourceAssetTreeModel::~SourceAssetTreeModel()
  24. {
  25. }
  26. void SourceAssetTreeModel::ResetModel()
  27. {
  28. // We need m_root to contain SourceAssetTreeItemData to show the stat column
  29. m_root.reset(new AssetTreeItem(
  30. AZStd::make_shared<SourceAssetTreeItemData>(nullptr, nullptr, "", "", true, AzToolsFramework::AssetDatabase::InvalidEntryId),
  31. m_errorIcon,
  32. m_folderIcon,
  33. m_fileIcon));
  34. if (ap_disableAssetTreeView)
  35. {
  36. return;
  37. }
  38. m_sourceToTreeItem.clear();
  39. m_sourceIdToTreeItem.clear();
  40. // Load stat table and attach matching stat to the source asset
  41. AZStd::unordered_map<AZStd::string, AZ::s64> statsTable;
  42. AZStd::string queryString{ "CreateJobs,%" };
  43. m_sharedDbConnection->QueryStatLikeStatName(
  44. queryString.c_str(),
  45. [&](AzToolsFramework::AssetDatabase::StatDatabaseEntry& stat)
  46. {
  47. static constexpr int numTokensExpected = 3;
  48. AZStd::vector<AZStd::string> tokens;
  49. AZ::StringFunc::Tokenize(stat.m_statName, tokens, ',');
  50. if (tokens.size() == numTokensExpected)
  51. {
  52. statsTable[tokens[1]] += stat.m_statValue;
  53. }
  54. else
  55. {
  56. AZ_Warning(
  57. "AssetProcessor",
  58. false,
  59. "Analysis Job (CreateJob) stat entry \"%s\" could not be parsed and will not be used. Expected %d tokens, but found %d. A wrong "
  60. "stat name may be used in Asset Processor code, or the asset database may be corrupted. If you keep encountering "
  61. "this warning, report an issue on GitHub with O3DE version number.",
  62. stat.m_statName.c_str(),
  63. numTokensExpected,
  64. tokens.size());
  65. }
  66. return true;
  67. });
  68. if (!m_intermediateAssets)
  69. {
  70. // AddOrUpdateEntry will remove intermediate assets if they shouldn't be included in this tree.
  71. m_sharedDbConnection->QuerySourceAndScanfolder(
  72. [&](AzToolsFramework::AssetDatabase::SourceAndScanFolderDatabaseEntry& sourceAndScanFolder)
  73. {
  74. if (statsTable.count(sourceAndScanFolder.m_sourceName))
  75. {
  76. AddOrUpdateEntry(sourceAndScanFolder, sourceAndScanFolder, true, statsTable[sourceAndScanFolder.m_sourceName]);
  77. }
  78. else
  79. {
  80. AddOrUpdateEntry(sourceAndScanFolder, sourceAndScanFolder, true);
  81. }
  82. return true; // return true to continue iterating over additional results, we are populating a container
  83. });
  84. }
  85. else
  86. {
  87. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolderEntry;
  88. IPathConversion* pathConversion = AZ::Interface<IPathConversion>::Get();
  89. if (!pathConversion || pathConversion->GetIntermediateAssetScanFolderId() == AzToolsFramework::AssetDatabase::InvalidEntryId)
  90. {
  91. // If, for some reason, the path conversion interface is not available, then try to retrieve intermediate folder information a different way.
  92. m_sharedDbConnection->QueryScanFolderByPortableKey(
  93. AssetProcessor::IntermediateAssetsFolderName,
  94. [&](AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& scanFolder)
  95. {
  96. scanFolderEntry = scanFolder;
  97. return false;
  98. });
  99. }
  100. else
  101. {
  102. m_sharedDbConnection->QueryScanFolderByScanFolderID(
  103. pathConversion->GetIntermediateAssetScanFolderId(),
  104. [&](AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& scanFolder)
  105. {
  106. scanFolderEntry = scanFolder;
  107. return false;
  108. });
  109. }
  110. m_sharedDbConnection->QuerySourceByScanFolderID(
  111. scanFolderEntry.m_scanFolderID,
  112. [&](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry)
  113. {
  114. if (statsTable.count(sourceEntry.m_sourceName))
  115. {
  116. AddOrUpdateEntry(sourceEntry, scanFolderEntry, true, statsTable[sourceEntry.m_sourceName]);
  117. }
  118. else
  119. {
  120. AddOrUpdateEntry(sourceEntry, scanFolderEntry, true);
  121. }
  122. return true; // return true to continue iterating over additional results, we are populating a container
  123. });
  124. }
  125. }
  126. void SourceAssetTreeModel::AddOrUpdateEntry(
  127. const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& source,
  128. const AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& scanFolder,
  129. bool modelIsResetting, AZ::s64 createJobDuration)
  130. {
  131. const auto& existingEntry = m_sourceToTreeItem.find(SourceAndScanID(source.m_sourceName, scanFolder.m_scanFolderID));
  132. if (existingEntry != m_sourceToTreeItem.end())
  133. {
  134. AZStd::shared_ptr<SourceAssetTreeItemData> sourceItemData = AZStd::rtti_pointer_cast<SourceAssetTreeItemData>(existingEntry->second->GetData());
  135. // This item already exists, refresh the related data.
  136. sourceItemData->m_scanFolderInfo = scanFolder;
  137. sourceItemData->m_sourceInfo = source;
  138. if (createJobDuration != -1)
  139. {
  140. // existing item: update duration only if it is provided
  141. sourceItemData->m_analysisDuration = createJobDuration;
  142. }
  143. QModelIndex existingIndexStart = createIndex(existingEntry->second->GetRow(), 0, existingEntry->second);
  144. QModelIndex existingIndexEnd = createIndex(existingEntry->second->GetRow(), existingEntry->second->GetColumnCount() - 1, existingEntry->second);
  145. dataChanged(existingIndexStart, existingIndexEnd);
  146. return;
  147. }
  148. AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolderEntry;
  149. IPathConversion* pathConversion = AZ::Interface<IPathConversion>::Get();
  150. AZ::s64 intermediateAssetScanFolder = AzToolsFramework::AssetDatabase::InvalidEntryId;
  151. if(pathConversion)
  152. {
  153. intermediateAssetScanFolder = pathConversion->GetIntermediateAssetScanFolderId();
  154. if (intermediateAssetScanFolder != AzToolsFramework::AssetDatabase::InvalidEntryId)
  155. {
  156. if (((source.m_scanFolderPK == intermediateAssetScanFolder && !m_intermediateAssets) ||
  157. (source.m_scanFolderPK != intermediateAssetScanFolder && m_intermediateAssets)))
  158. {
  159. return;
  160. }
  161. }
  162. }
  163. AZ::IO::Path fullPath = AZ::IO::Path(scanFolder.m_scanFolder) / source.m_sourceName;
  164. // It's common for Open 3D Engine game projects and scan folders to be in a subfolder
  165. // of the engine install. To improve readability of the source files, strip out
  166. // that portion of the path if it overlaps.
  167. if (!m_assetRootSet)
  168. {
  169. m_assetRootSet = AssetUtilities::ComputeAssetRoot(m_assetRoot, nullptr);
  170. }
  171. if (m_assetRootSet)
  172. {
  173. fullPath = fullPath.LexicallyProximate(m_assetRoot.absolutePath().toUtf8().constData());
  174. }
  175. if (fullPath.empty())
  176. {
  177. AZ_Warning(
  178. "AssetProcessor", false, "Source id %s has an invalid name: %s", source.m_sourceGuid.ToString<AZStd::string>().c_str(),
  179. source.m_sourceName.c_str());
  180. return;
  181. }
  182. AssetTreeItem* parentItem = m_root.get();
  183. // Use posix path separator for each child item
  184. AZ::IO::Path currentFullFolderPath(AZ::IO::PosixPathSeparator);
  185. const AZ::IO::FixedMaxPath filename = fullPath.Filename();
  186. fullPath.RemoveFilename();
  187. AZ::IO::FixedMaxPathString currentPath;
  188. for (auto pathIt = fullPath.begin(); pathIt != fullPath.end(); ++pathIt)
  189. {
  190. currentPath = pathIt->FixedMaxPathString();
  191. currentFullFolderPath /= currentPath;
  192. AssetTreeItem* nextParent = parentItem->GetChildFolder(currentPath.c_str());
  193. if (!nextParent)
  194. {
  195. if (!modelIsResetting)
  196. {
  197. QModelIndex parentIndex = parentItem == m_root.get() ? QModelIndex() : createIndex(parentItem->GetRow(), 0, parentItem);
  198. Q_ASSERT(checkIndex(parentIndex));
  199. beginInsertRows(parentIndex, parentItem->getChildCount(), parentItem->getChildCount());
  200. }
  201. nextParent = parentItem->CreateChild(AZStd::make_shared<SourceAssetTreeItemData>(
  202. nullptr,
  203. nullptr,
  204. currentFullFolderPath.Native(),
  205. currentPath.c_str(),
  206. true,
  207. scanFolder.m_scanFolderID));
  208. m_sourceToTreeItem[SourceAndScanID(currentFullFolderPath.Native(), scanFolder.m_scanFolderID)] = nextParent;
  209. // Folders don't have source IDs, don't add to m_sourceIdToTreeItem
  210. if (!modelIsResetting)
  211. {
  212. endInsertRows();
  213. }
  214. }
  215. parentItem = nextParent;
  216. }
  217. if (!modelIsResetting)
  218. {
  219. QModelIndex parentIndex = parentItem == m_root.get() ? QModelIndex() : createIndex(parentItem->GetRow(), 0, parentItem);
  220. Q_ASSERT(checkIndex(parentIndex));
  221. beginInsertRows(parentIndex, parentItem->getChildCount(), parentItem->getChildCount());
  222. }
  223. m_sourceToTreeItem[SourceAndScanID(source.m_sourceName, scanFolder.m_scanFolderID)] =
  224. parentItem->CreateChild(AZStd::make_shared<SourceAssetTreeItemData>(
  225. &source,
  226. &scanFolder,
  227. source.m_sourceName,
  228. AZ::IO::FixedMaxPathString(filename.Native()).c_str(),
  229. false,
  230. scanFolder.m_scanFolderID,
  231. createJobDuration));
  232. m_sourceIdToTreeItem[source.m_sourceID] = m_sourceToTreeItem[SourceAndScanID(source.m_sourceName, scanFolder.m_scanFolderID)];
  233. if (!modelIsResetting)
  234. {
  235. endInsertRows();
  236. }
  237. }
  238. void SourceAssetTreeModel::OnSourceFileChanged(const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
  239. {
  240. if (ap_disableAssetTreeView)
  241. {
  242. return;
  243. }
  244. if (!m_root)
  245. {
  246. // we haven't reset the model yet, which means all of this will happen when we do.
  247. return;
  248. }
  249. // Model changes need to be run on the main thread.
  250. AZ::SystemTickBus::QueueFunction([&, entry]()
  251. {
  252. m_sharedDbConnection->QueryScanFolderBySourceID(entry.m_sourceID,
  253. [&, entry](AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry& scanFolder)
  254. {
  255. AddOrUpdateEntry(entry, scanFolder, false);
  256. return true;
  257. });
  258. });
  259. }
  260. void SourceAssetTreeModel::RemoveFoldersIfEmpty(AssetTreeItem* itemToCheck)
  261. {
  262. // Don't attempt to remove invalid items, non-folders, folders that still have items in them, or the root.
  263. if (!itemToCheck || !itemToCheck->GetData()->m_isFolder || itemToCheck->getChildCount() > 0 || !itemToCheck->GetParent())
  264. {
  265. return;
  266. }
  267. RemoveAssetTreeItem(itemToCheck);
  268. }
  269. void SourceAssetTreeModel::RemoveAssetTreeItem(AssetTreeItem* assetToRemove)
  270. {
  271. if (!assetToRemove)
  272. {
  273. return;
  274. }
  275. AssetTreeItem* parent = assetToRemove->GetParent();
  276. if (!parent)
  277. {
  278. return;
  279. }
  280. QModelIndex parentIndex = parent == m_root.get() ? QModelIndex() : createIndex(parent->GetRow(), 0, parent);
  281. Q_ASSERT(checkIndex(parentIndex));
  282. beginRemoveRows(parentIndex, assetToRemove->GetRow(), assetToRemove->GetRow());
  283. m_sourceToTreeItem.erase(SourceAndScanID(assetToRemove->GetData()->m_assetDbName, assetToRemove->GetData()->m_scanFolderID));
  284. const AZStd::shared_ptr<const SourceAssetTreeItemData> sourceItemData = AZStd::rtti_pointer_cast<const SourceAssetTreeItemData>(assetToRemove->GetData());
  285. if (sourceItemData && sourceItemData->m_hasDatabaseInfo)
  286. {
  287. m_sourceIdToTreeItem.erase(sourceItemData->m_sourceInfo.m_sourceID);
  288. }
  289. parent->EraseChild(assetToRemove);
  290. endRemoveRows();
  291. RemoveFoldersIfEmpty(parent);
  292. }
  293. void SourceAssetTreeModel::OnSourceFileRemoved(AZ::s64 sourceId)
  294. {
  295. if (ap_disableAssetTreeView)
  296. {
  297. return;
  298. }
  299. if (!m_root)
  300. {
  301. // we haven't reset the model yet, which means all of this will happen when we do.
  302. return;
  303. }
  304. // UI changes need to be done on the main thread.
  305. AZ::SystemTickBus::QueueFunction([&, sourceId]()
  306. {
  307. auto existingSource = m_sourceIdToTreeItem.find(sourceId);
  308. if (existingSource == m_sourceIdToTreeItem.end() || !existingSource->second)
  309. {
  310. // If the asset being removed wasn't previously cached, then something has gone wrong. Reset the model.
  311. Reset();
  312. return;
  313. }
  314. RemoveAssetTreeItem(existingSource->second);
  315. });
  316. }
  317. QVariant SourceAssetTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
  318. {
  319. if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
  320. {
  321. return QVariant();
  322. }
  323. if (section < 0 || section >= static_cast<int>(SourceAssetTreeColumns::Max))
  324. {
  325. return QVariant();
  326. }
  327. switch (section)
  328. {
  329. case aznumeric_cast<int>(SourceAssetTreeColumns::AnalysisJobDuration):
  330. return tr("Last Analysis Job Duration");
  331. default:
  332. return AssetTreeModel::headerData(section, orientation, role);
  333. }
  334. }
  335. QModelIndex SourceAssetTreeModel::GetIndexForSource(const AZStd::string& source, AZ::s64 scanFolderID)
  336. {
  337. if (ap_disableAssetTreeView)
  338. {
  339. return QModelIndex();
  340. }
  341. auto sourceItem = m_sourceToTreeItem.find(SourceAndScanID(source, scanFolderID));
  342. if (sourceItem == m_sourceToTreeItem.end())
  343. {
  344. return QModelIndex();
  345. }
  346. return createIndex(sourceItem->second->GetRow(), 0, sourceItem->second);
  347. }
  348. void SourceAssetTreeModel::OnCreateJobsDurationChanged(QString sourceName, AZ::s64 scanFolderID)
  349. {
  350. // update the source asset's CreateJob duration, if such asset exists in the tree
  351. const auto& existingEntry = m_sourceToTreeItem.find(SourceAndScanID(sourceName.toUtf8().constData(), scanFolderID));
  352. if (existingEntry != m_sourceToTreeItem.end())
  353. {
  354. AZStd::shared_ptr<SourceAssetTreeItemData> sourceItemData =
  355. AZStd::rtti_pointer_cast<SourceAssetTreeItemData>(existingEntry->second->GetData());
  356. AZ::s64 accumulateJobDuration = 0;
  357. QString statKey = QString("CreateJobs,%1").arg(sourceName).append("%");
  358. m_sharedDbConnection->QueryStatLikeStatName(
  359. statKey.toUtf8().data(),
  360. [&](AzToolsFramework::AssetDatabase::StatDatabaseEntry statEntry)
  361. {
  362. accumulateJobDuration += statEntry.m_statValue;
  363. return true;
  364. });
  365. sourceItemData->m_analysisDuration = accumulateJobDuration;
  366. QModelIndex existingIndex = createIndex(
  367. existingEntry->second->GetRow(), aznumeric_cast<int>(SourceAssetTreeColumns::AnalysisJobDuration), existingEntry->second);
  368. dataChanged(existingIndex, existingIndex);
  369. }
  370. }
  371. }