AggregateAdapter.cpp 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  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 <AzFramework/DocumentPropertyEditor/AggregateAdapter.h>
  9. namespace AZ::DocumentPropertyEditor
  10. {
  11. RowAggregateAdapter::RowAggregateAdapter()
  12. : m_rootNode(AZStd::make_unique<AggregateNode>())
  13. {
  14. }
  15. RowAggregateAdapter::~RowAggregateAdapter()
  16. {
  17. }
  18. void RowAggregateAdapter::AddAdapter(DocumentAdapterPtr sourceAdapter)
  19. {
  20. // capture the actual adapter, not just the index, as adding or removing adapters could change the index
  21. auto& newAdapterInfo = m_adapters.emplace_back(AZStd::make_unique<AdapterInfo>());
  22. newAdapterInfo->adapter = sourceAdapter;
  23. newAdapterInfo->resetHandler = DocumentAdapter::ResetEvent::Handler(
  24. [this, sourceAdapter]()
  25. {
  26. this->HandleAdapterReset(sourceAdapter);
  27. });
  28. sourceAdapter->ConnectResetHandler(newAdapterInfo->resetHandler);
  29. newAdapterInfo->changedHandler = DocumentAdapter::ChangedEvent::Handler(
  30. [this, sourceAdapter](const Dom::Patch& patch)
  31. {
  32. this->HandleDomChange(sourceAdapter, patch);
  33. });
  34. sourceAdapter->ConnectChangedHandler(newAdapterInfo->changedHandler);
  35. newAdapterInfo->domMessageHandler = AZ::DocumentPropertyEditor::DocumentAdapter::MessageEvent::Handler(
  36. [this, sourceAdapter](const AZ::DocumentPropertyEditor::AdapterMessage& message, AZ::Dom::Value& value)
  37. {
  38. this->HandleDomMessage(sourceAdapter, message, value);
  39. });
  40. sourceAdapter->ConnectMessageHandler(newAdapterInfo->domMessageHandler);
  41. const auto adapterIndex = m_adapters.size() - 1;
  42. PopulateNodesForAdapter(adapterIndex);
  43. NotifyResetDocument();
  44. }
  45. void RowAggregateAdapter::RemoveAdapter(DocumentAdapterPtr sourceAdapter)
  46. {
  47. // TODO: https://github.com/o3de/o3de/issues/15609
  48. (void)sourceAdapter;
  49. }
  50. void RowAggregateAdapter::ClearAdapters()
  51. {
  52. m_adapters.clear();
  53. m_rootNode = AZStd::make_unique<AggregateNode>();
  54. NotifyResetDocument();
  55. }
  56. ExpanderSettings* RowAggregateAdapter::CreateExpanderSettings(
  57. DocumentAdapter* referenceAdapter, const AZStd::string& settingsRegistryKey, const AZStd::string& propertyEditorName)
  58. {
  59. AZ_Assert(!m_adapters.empty(), "RowAggregateAdapter::CreateExpanderSettings called before any adapters were added!");
  60. return m_adapters[0]->adapter->CreateExpanderSettings(referenceAdapter, settingsRegistryKey, propertyEditorName);
  61. }
  62. bool RowAggregateAdapter::AggregateNode::HasEntryForAdapter(size_t adapterIndex)
  63. {
  64. return (m_pathEntries.size() > adapterIndex && m_pathEntries[adapterIndex] != InvalidEntry);
  65. }
  66. Dom::Path RowAggregateAdapter::AggregateNode::GetPathForAdapter(size_t adapterIndex)
  67. {
  68. Dom::Path pathForAdapter;
  69. auto addParentThenSelf = [&pathForAdapter, adapterIndex](AggregateNode* currentNode, auto&& addParentThenSelf) -> bool
  70. {
  71. bool succeeded = true;
  72. if (currentNode->m_parent)
  73. {
  74. succeeded = addParentThenSelf(currentNode->m_parent, addParentThenSelf);
  75. }
  76. if (succeeded && adapterIndex < currentNode->m_pathEntries.size())
  77. {
  78. const size_t pathEntry = currentNode->m_pathEntries[adapterIndex];
  79. succeeded = (pathEntry != InvalidEntry);
  80. if (succeeded)
  81. {
  82. pathForAdapter.Push(pathEntry);
  83. }
  84. }
  85. return succeeded;
  86. };
  87. auto validPath = addParentThenSelf(this, addParentThenSelf);
  88. return (validPath ? pathForAdapter : Dom::Path());
  89. }
  90. void RowAggregateAdapter::AggregateNode::AddEntry(size_t adapterIndex, size_t pathEntryIndex, bool matchesOtherEntries)
  91. {
  92. const auto currentSize = m_pathEntries.size();
  93. if (currentSize <= adapterIndex)
  94. {
  95. m_pathEntries.resize(adapterIndex + 1, InvalidEntry);
  96. }
  97. m_pathEntries[adapterIndex] = pathEntryIndex;
  98. // set m_allEntriesMatch so that the Adapter knows whether to show a "values differ" type of node
  99. m_allEntriesMatch = matchesOtherEntries;
  100. AZ_Assert(m_parent, "Attempt to add entry to orphaned node!");
  101. if (m_parent)
  102. {
  103. // add mapping to parent so that we can easily walk an adapter's path to get to the desired AggregateNode
  104. const auto parentSize = m_parent->m_pathIndexToChildMaps.size();
  105. if (parentSize <= adapterIndex)
  106. {
  107. m_parent->m_pathIndexToChildMaps.resize(adapterIndex + 1);
  108. }
  109. else
  110. {
  111. // shift any subsequent entries by 1 to make room for this new entry, then add it
  112. m_parent->ShiftChildIndices(adapterIndex, pathEntryIndex, 1);
  113. }
  114. m_parent->m_pathIndexToChildMaps[adapterIndex][pathEntryIndex] = this;
  115. }
  116. }
  117. size_t RowAggregateAdapter::AggregateNode::GetComparisonAdapterIndex(size_t omitAdapterIndex)
  118. {
  119. for (size_t currIndex = 0, numIndices = m_pathEntries.size(); currIndex < numIndices; ++currIndex)
  120. {
  121. if (currIndex != omitAdapterIndex && m_pathEntries[currIndex] != AggregateNode::InvalidEntry)
  122. {
  123. return currIndex;
  124. }
  125. }
  126. return AggregateNode::InvalidEntry;
  127. }
  128. bool RowAggregateAdapter::AggregateNode::RemoveEntry(size_t adapterIndex)
  129. {
  130. // first recurse to remove entries from the bottom up
  131. if (m_pathIndexToChildMaps.size() > adapterIndex)
  132. {
  133. auto& childMap = m_pathIndexToChildMaps[adapterIndex];
  134. while (!childMap.empty())
  135. {
  136. childMap.begin()->second->RemoveEntry(adapterIndex);
  137. }
  138. }
  139. // find entry in parent's index map and remove it
  140. AZ_Assert(m_parent->m_pathIndexToChildMaps.size() > adapterIndex, "attempt to remove entry with invalid adapterIndex!");
  141. auto& parentsMap = m_parent->m_pathIndexToChildMaps[adapterIndex];
  142. auto mapEntryIter = AZStd::find_if(
  143. parentsMap.begin(),
  144. parentsMap.end(),
  145. [this](auto& mapEntry)
  146. {
  147. return (mapEntry.second == this);
  148. });
  149. AZ_Assert(mapEntryIter != parentsMap.end(), "child must be present in parent's map!");
  150. // remove entry from map and shift any later siblings' indices down by one
  151. parentsMap.erase(mapEntryIter);
  152. m_parent->ShiftChildIndices(adapterIndex, mapEntryIter->first, -1);
  153. bool nodeWasDestroyed = false;
  154. m_pathEntries[adapterIndex] = InvalidEntry;
  155. auto remainingEntries = m_pathEntries.size();
  156. while (remainingEntries && m_pathEntries[remainingEntries - 1] == InvalidEntry)
  157. {
  158. --remainingEntries;
  159. }
  160. if (!remainingEntries)
  161. {
  162. // no remaining entries, destroy this node
  163. auto* parentNode = m_parent;
  164. auto& parentsChildList = parentNode->m_childRows;
  165. auto thisNodeIter = AZStd::find_if(
  166. parentsChildList.begin(),
  167. parentsChildList.end(),
  168. [this](auto& mapEntry)
  169. {
  170. return (mapEntry.get() == this);
  171. });
  172. AZ_Assert(thisNodeIter != parentsChildList.end(), "child must be present in parent's list!");
  173. // entry is a unique_ptr, erasing it will destroy this node
  174. parentsChildList.erase(thisNodeIter);
  175. nodeWasDestroyed = true;
  176. }
  177. else if (remainingEntries < m_pathEntries.size())
  178. {
  179. // some valid entries remain, resize appropriately
  180. m_pathEntries.resize(remainingEntries);
  181. }
  182. return nodeWasDestroyed;
  183. }
  184. size_t RowAggregateAdapter::AggregateNode::EntryCount() const
  185. {
  186. // count all entries that are not InvalidEntry
  187. return AZStd::count_if(
  188. m_pathEntries.begin(),
  189. m_pathEntries.end(),
  190. [&](size_t currentEntry)
  191. {
  192. return (currentEntry != InvalidEntry);
  193. });
  194. }
  195. void RowAggregateAdapter::AggregateNode::ShiftChildIndices(size_t adapterIndex, size_t startIndex, int delta)
  196. {
  197. if(adapterIndex < m_pathIndexToChildMaps.size())
  198. {
  199. auto& adapterChildMap = m_pathIndexToChildMaps[adapterIndex];
  200. auto firstEntry = adapterChildMap.lower_bound(startIndex);
  201. if (firstEntry != adapterChildMap.end())
  202. {
  203. auto applyDelta = [adapterIndex, delta, &adapterChildMap](auto& iterator)
  204. {
  205. // make an iterator that points to the next element for the re-insertion hint
  206. auto reinsertPos = iterator;
  207. ++reinsertPos;
  208. auto entry = adapterChildMap.extract(iterator);
  209. entry.mapped()->m_pathEntries[adapterIndex] += delta;
  210. entry.key() += delta;
  211. iterator = adapterChildMap.insert(reinsertPos, AZStd::move(entry));
  212. };
  213. const bool incrementing = (delta > 0);
  214. if (incrementing)
  215. {
  216. /* we don't want the map entries to pass each other and require the internal map to change structure,
  217. so start at the end and move backwards if we're incrementing the indices */
  218. auto mapIter = adapterChildMap.end();
  219. do
  220. {
  221. // we want to applyDelta up to and including the firstEntry
  222. --mapIter;
  223. applyDelta(mapIter);
  224. } while (mapIter != firstEntry);
  225. }
  226. else
  227. {
  228. // not incrementing, so we're removing from the front this time
  229. for (auto mapIter = firstEntry; mapIter != adapterChildMap.end(); ++mapIter)
  230. {
  231. applyDelta(mapIter);
  232. }
  233. }
  234. }
  235. }
  236. }
  237. void RowAggregateAdapter::HandleAdapterReset(DocumentAdapterPtr adapter)
  238. {
  239. // TODO https://github.com/o3de/o3de/issues/15610
  240. (void)adapter;
  241. }
  242. void RowAggregateAdapter::HandleDomChange(DocumentAdapterPtr adapter, const Dom::Patch& patch)
  243. {
  244. const auto adapterIndex = GetIndexForAdapter(adapter);
  245. Dom::Patch outgoingPatch;
  246. for (auto operationIterator = patch.begin(), endIterator = patch.end(); operationIterator != endIterator; ++operationIterator)
  247. {
  248. const auto& patchOperation = *operationIterator;
  249. const auto& patchPath = patchOperation.GetDestinationPath();
  250. if (patchOperation.GetType() == AZ::Dom::PatchOperation::Type::Remove)
  251. {
  252. Dom::Path leftoverPath;
  253. auto* nodeAtPath = GetNodeAtAdapterPath(adapterIndex, patchPath, leftoverPath);
  254. AZ_Assert(nodeAtPath, "got a patch for a path that has no node!");
  255. if (leftoverPath.IsEmpty())
  256. {
  257. // this removal was a row, remove its node entry
  258. ProcessRemoval(nodeAtPath, adapterIndex, &outgoingPatch);
  259. }
  260. else
  261. {
  262. if (leftoverPath.Size() == 1)
  263. {
  264. // this removal was not a full row, but it was the immediate child of a row
  265. // this removal shifts subsequent entries, so account for that in the parent
  266. nodeAtPath->ShiftChildIndices(adapterIndex, patchPath.Back().GetIndex(), -1);
  267. }
  268. // removing a column or attribute entry might affect the row it belongs to. Update it
  269. if (nodeAtPath->GetComparisonAdapterIndex() == adapterIndex)
  270. {
  271. UpdateAndPatchNode(nodeAtPath, adapterIndex, patchOperation, leftoverPath, outgoingPatch);
  272. }
  273. }
  274. }
  275. else if (patchOperation.GetType() == AZ::Dom::PatchOperation::Type::Replace)
  276. {
  277. Dom::Path leftoverPath;
  278. auto* rowNode = GetNodeAtAdapterPath(adapterIndex, patchPath, leftoverPath);
  279. const bool replacementIsRow = IsRow(patchOperation.GetValue());
  280. if (!leftoverPath.IsEmpty())
  281. {
  282. // there's leftover path so its either a column or attribute that's being patched
  283. // Find and update the first row node in this patch's ancestry
  284. if (replacementIsRow)
  285. {
  286. // handle column being replaced by row. Add the new row child
  287. AddChildRow(adapterIndex, rowNode, patchOperation.GetValue(), patchPath.Back().GetIndex(), &outgoingPatch);
  288. // the replace operation is now just a removal, since the add part has been handled. Change the patchOperation
  289. // accordingly, and update the row node
  290. auto removalOperation = Dom::PatchOperation::RemoveOperation(patchOperation.GetDestinationPath());
  291. UpdateAndPatchNode(rowNode, adapterIndex, removalOperation, leftoverPath, outgoingPatch);
  292. }
  293. else
  294. {
  295. // neither the existing nor the replacement values are rows, pass the operation through and update the row node
  296. UpdateAndPatchNode(rowNode, adapterIndex, patchOperation, leftoverPath, outgoingPatch);
  297. }
  298. }
  299. else
  300. {
  301. // handle row being replaced
  302. if (replacementIsRow)
  303. {
  304. auto parentNode = rowNode->m_parent; // cache of parent, since nodeAtPath may be imminently removed
  305. ProcessRemoval(rowNode, adapterIndex, &outgoingPatch);
  306. AddChildRow(adapterIndex, parentNode, patchOperation.GetValue(), patchPath.Back().GetIndex(), &outgoingPatch);
  307. }
  308. else
  309. {
  310. // replacing a row with a non-row. Remove the existing row, then update the parent
  311. ProcessRemoval(rowNode, adapterIndex, &outgoingPatch);
  312. // replace operation is now just an add, since the removal part has been handled. Change the
  313. // patchOperation accordingly
  314. auto addOperation =
  315. Dom::PatchOperation::AddOperation(patchOperation.GetDestinationPath(), patchOperation.GetValue());
  316. UpdateAndPatchNode(rowNode->m_parent, adapterIndex, addOperation, leftoverPath, outgoingPatch);
  317. }
  318. }
  319. }
  320. else if (patchOperation.GetType() == AZ::Dom::PatchOperation::Type::Add)
  321. {
  322. Dom::Path leftoverPath;
  323. auto rowNode = GetNodeAtAdapterPath(adapterIndex, patchPath, leftoverPath);
  324. const bool rowIsDirectParent = (leftoverPath.Size() == 1);
  325. if (rowIsDirectParent && IsRow(patchOperation.GetValue()))
  326. {
  327. // adding a full row to the root or another row
  328. AZ_Assert(leftoverPath.Back().IsIndex(), "new addition is a row, it must be addressed by index!");
  329. const auto childIndex = leftoverPath.Back().GetIndex();
  330. AddChildRow(adapterIndex, rowNode, patchOperation.GetValue(), childIndex, &outgoingPatch);
  331. }
  332. else
  333. {
  334. // either a column or an attribute was added, update the nearest row as necessary
  335. if (rowIsDirectParent)
  336. {
  337. if (auto backIndex = patchPath.Back(); backIndex.IsIndex())
  338. {
  339. // the added child was not a full row, but it was the immediate child of a row
  340. // this addition shifts subsequent entries, so account for that in the parent
  341. rowNode->ShiftChildIndices(adapterIndex, backIndex.GetIndex(), 1);
  342. }
  343. }
  344. UpdateAndPatchNode(rowNode, adapterIndex, patchOperation, leftoverPath, outgoingPatch);
  345. }
  346. }
  347. else
  348. {
  349. // Note that some patch operations (move and copy) are still unsupported at this time
  350. // Please see: https://github.com/o3de/o3de/issues/15612
  351. AZ_Assert(0, "RowAggregateAdapter: patch operation type not supported yet!");
  352. }
  353. }
  354. if (outgoingPatch.Size())
  355. {
  356. NotifyContentsChanged(outgoingPatch);
  357. }
  358. }
  359. void RowAggregateAdapter::HandleDomMessage(
  360. [[maybe_unused]] DocumentAdapterPtr adapter,
  361. [[maybe_unused]] const AZ::DocumentPropertyEditor::AdapterMessage& message,
  362. [[maybe_unused]] Dom::Value& value)
  363. {
  364. // TODO forwarding all of these isn't desirable, test to see if we need to conditionally forward this
  365. // https://github.com/o3de/o3de/issues/15611
  366. }
  367. void RowAggregateAdapter::EvaluateAllEntriesMatch(AggregateNode* node)
  368. {
  369. bool allEntriesMatch = true;
  370. if (node->EntryCount() > 1)
  371. {
  372. auto comparisonRow = m_adapters[0]->adapter->GetContents()[node->GetPathForAdapter(0)];
  373. for (size_t index = 1, numEntries = node->m_pathEntries.size(); index < numEntries && allEntriesMatch; ++index)
  374. {
  375. if (node->m_pathEntries[index] != AggregateNode::InvalidEntry)
  376. {
  377. auto currAdapterPath = node->GetPathForAdapter(index);
  378. AZ_Assert(!currAdapterPath.IsEmpty(), "ancestorRowNode had an entry for this adapter -- it must have a valid path!");
  379. auto currRowValue = m_adapters[index]->adapter->GetContents()[currAdapterPath];
  380. allEntriesMatch = ValuesMatch(comparisonRow, currRowValue);
  381. }
  382. }
  383. }
  384. node->m_allEntriesMatch = allEntriesMatch;
  385. }
  386. size_t RowAggregateAdapter::GetIndexForAdapter(const DocumentAdapterPtr& adapter)
  387. {
  388. for (size_t adapterIndex = 0, numAdapters = m_adapters.size(); adapterIndex < numAdapters; ++adapterIndex)
  389. {
  390. if (m_adapters[adapterIndex]->adapter == adapter)
  391. return adapterIndex;
  392. }
  393. AZ_Warning("RowAggregateAdapter", false, "GetIndexForAdapter called with unknown adapter!");
  394. return size_t(-1);
  395. }
  396. RowAggregateAdapter::AggregateNode* RowAggregateAdapter::GetNodeAtAdapterPath(
  397. size_t adapterIndex, const Dom::Path& path, Dom::Path& leftoverPath)
  398. {
  399. AggregateNode* currNode = m_rootNode.get();
  400. if (path.Size() < 1)
  401. {
  402. // path is empty, return the root node
  403. return currNode;
  404. }
  405. // quick lambda to add any unconsumed path to leftoverPath
  406. auto addRemainingPath = [&leftoverPath](auto pathIter, auto endIter)
  407. {
  408. while (pathIter != endIter)
  409. {
  410. leftoverPath.Push(*pathIter);
  411. ++pathIter;
  412. }
  413. };
  414. for (auto pathIter = path.begin(), endIter = path.end(); pathIter != endIter; ++pathIter)
  415. {
  416. const auto& pathEntry = *pathIter;
  417. if (!pathEntry.IsIndex() || adapterIndex >= currNode->m_pathIndexToChildMaps.size())
  418. {
  419. // this path includes a non-index entry or an index out of bounds -- we can search no deeper
  420. addRemainingPath(pathIter, endIter);
  421. return currNode;
  422. }
  423. auto& pathMap = currNode->m_pathIndexToChildMaps[adapterIndex];
  424. const auto index = pathEntry.GetIndex();
  425. if (auto foundIter = pathMap.find(index); foundIter != pathMap.end())
  426. {
  427. currNode = foundIter->second;
  428. }
  429. else
  430. {
  431. // nothing for that path entry. If pathMatch is NearestRow, assume this is a column entry,
  432. // and return the most recent row node
  433. addRemainingPath(pathIter, endIter);
  434. return currNode;
  435. }
  436. }
  437. return currNode;
  438. }
  439. RowAggregateAdapter::AggregateNode* RowAggregateAdapter::GetNodeAtPath(const Dom::Path& aggregatePath)
  440. {
  441. AggregateNode* currNode = m_rootNode.get();
  442. if (aggregatePath.Size() < 1)
  443. {
  444. // path is empty, return the root node
  445. return currNode;
  446. }
  447. auto getCompleteChildAtIndex = [&](AggregateNode* node, size_t childIndex) -> AggregateNode*
  448. {
  449. size_t numCompleteRows = 0;
  450. size_t testIndex = 0;
  451. while (numCompleteRows <= childIndex && testIndex < node->m_childRows.size())
  452. {
  453. auto& currChild = node->m_childRows[testIndex];
  454. if (NodeIsComplete(currChild.get()))
  455. {
  456. ++numCompleteRows;
  457. }
  458. if (numCompleteRows > childIndex)
  459. {
  460. return node->m_childRows[testIndex].get();
  461. }
  462. ++testIndex;
  463. }
  464. return nullptr;
  465. };
  466. for (const auto& pathEntry : aggregatePath)
  467. {
  468. if (!pathEntry.IsIndex())
  469. {
  470. // this path includes a non-index entry, and is therefore not a row
  471. return nullptr;
  472. }
  473. const auto index = pathEntry.GetIndex();
  474. currNode = getCompleteChildAtIndex(currNode, index);
  475. if (!currNode)
  476. {
  477. return nullptr;
  478. }
  479. }
  480. return currNode;
  481. }
  482. Dom::Path RowAggregateAdapter::GetPathForNode(AggregateNode* node)
  483. {
  484. // verify that this and all ancestors have entries for each adapter,
  485. // otherwise there is no path for this node, as it won't be included in the contents
  486. auto* currNode = node;
  487. while (currNode)
  488. {
  489. if (currNode != m_rootNode.get() && !NodeIsComplete(currNode))
  490. {
  491. return Dom::Path();
  492. }
  493. currNode = currNode->m_parent;
  494. }
  495. Dom::Path nodePath;
  496. auto addParentThenSelf = [this, &nodePath](AggregateNode* currentNode, auto&& addParentThenSelf) -> void
  497. {
  498. auto* currParent = currentNode->m_parent;
  499. if (currParent)
  500. {
  501. addParentThenSelf(currParent, addParentThenSelf);
  502. size_t currIndex = 0;
  503. for (auto& currChild : currParent->m_childRows)
  504. {
  505. if (currChild.get() == currentNode)
  506. {
  507. nodePath.Push(currIndex);
  508. break;
  509. }
  510. else if (NodeIsComplete(currChild.get()))
  511. {
  512. ++currIndex;
  513. }
  514. }
  515. }
  516. };
  517. addParentThenSelf(node, addParentThenSelf);
  518. return nodePath;
  519. }
  520. Dom::Value RowAggregateAdapter::GetComparisonRow(AggregateNode* aggregateNode, size_t omitAdapterIndex)
  521. {
  522. auto adapterIndex = aggregateNode->GetComparisonAdapterIndex(omitAdapterIndex);
  523. AZ_Assert(
  524. adapterIndex != AggregateNode::InvalidEntry,
  525. "you should not be trying to generate a comparison row for a node without a valid adapter entry!");
  526. Dom::Path pathToComparisonValue = aggregateNode->GetPathForAdapter(adapterIndex);
  527. auto comparisonRow = m_adapters[adapterIndex]->adapter->GetContents()[pathToComparisonValue];
  528. RemoveChildRows(comparisonRow);
  529. return comparisonRow;
  530. }
  531. Dom::Value RowAggregateAdapter::GetValueForNode(AggregateNode* aggregateNode)
  532. {
  533. return (
  534. aggregateNode->m_allEntriesMatch || !m_generateDiffRows ? GenerateAggregateRow(aggregateNode)
  535. : GenerateValuesDifferRow(aggregateNode));
  536. }
  537. Dom::Value RowAggregateAdapter::GetValueHierarchyForNode(AggregateNode* aggregateNode)
  538. {
  539. Dom::Value returnValue;
  540. // only create a value if this node is represented by all member adapters
  541. if (aggregateNode && NodeIsComplete(aggregateNode))
  542. {
  543. // create a row value for this node
  544. returnValue = Dom::Value::CreateNode(Nodes::Row::Name);
  545. /* add all row children first (before any labels or PropertyHandlers in this row),
  546. so that functions like GetPathForNode can make simplifying assumptions */
  547. for (auto& currChild : aggregateNode->m_childRows)
  548. {
  549. Dom::Value childValue = GetValueHierarchyForNode(currChild.get());
  550. if (!childValue.IsNull())
  551. {
  552. returnValue.ArrayPushBack(childValue);
  553. }
  554. }
  555. // row children have been added, now add the actual label/PropertyEditor children
  556. auto childlessRow = GetValueForNode(aggregateNode);
  557. if (!childlessRow.IsArrayEmpty())
  558. {
  559. returnValue.ArrayReserve(returnValue.ArraySize() + childlessRow.ArraySize());
  560. AZStd::move(
  561. childlessRow.MutableArrayBegin(), childlessRow.MutableArrayEnd(), AZStd::back_inserter(returnValue.GetMutableArray()));
  562. }
  563. }
  564. return returnValue;
  565. }
  566. void RowAggregateAdapter::PopulateNodesForAdapter(size_t adapterIndex)
  567. {
  568. auto sourceAdapter = m_adapters[adapterIndex]->adapter;
  569. const auto& sourceContents = sourceAdapter->GetContents();
  570. PopulateChildren(adapterIndex, sourceContents, m_rootNode.get());
  571. }
  572. RowAggregateAdapter::AggregateNode* RowAggregateAdapter::AddChildRow(
  573. size_t adapterIndex, AggregateNode* parentNode, const Dom::Value& childValue, size_t childIndex, Dom::Patch* outgoingPatch)
  574. {
  575. AggregateNode* addedToNode = nullptr;
  576. // check each existing child to see if we belong there.
  577. for (auto matchIter = parentNode->m_childRows.begin(), endIter = parentNode->m_childRows.end();
  578. !addedToNode && matchIter != endIter;
  579. ++matchIter)
  580. {
  581. AggregateNode* possibleMatch = matchIter->get();
  582. // make sure there isn't already an entry for this adapter. This can happen in
  583. // edge cases where multiple rows can match, like in multi-sets
  584. if (!possibleMatch->HasEntryForAdapter(adapterIndex))
  585. {
  586. auto comparisonRow = GetComparisonRow(possibleMatch);
  587. if (SameRow(childValue, comparisonRow))
  588. {
  589. const bool allEntriesMatch = possibleMatch->m_allEntriesMatch && ValuesMatch(childValue, comparisonRow);
  590. possibleMatch->AddEntry(adapterIndex, childIndex, allEntriesMatch);
  591. PopulateChildren(adapterIndex, childValue, possibleMatch);
  592. addedToNode = possibleMatch;
  593. }
  594. }
  595. }
  596. if (!addedToNode)
  597. {
  598. // didn't find an existing child to own us, add a new one and attach to it
  599. auto& newNode = parentNode->m_childRows.emplace_back(AZStd::make_unique<AggregateNode>());
  600. newNode->m_parent = parentNode;
  601. newNode->AddEntry(adapterIndex, childIndex, true);
  602. PopulateChildren(adapterIndex, childValue, newNode.get());
  603. addedToNode = newNode.get();
  604. }
  605. if (addedToNode && outgoingPatch && NodeIsComplete(addedToNode))
  606. {
  607. // the aggregate node that this add affected is now complete
  608. outgoingPatch->PushBack(Dom::PatchOperation::AddOperation(GetPathForNode(addedToNode), GetValueHierarchyForNode(addedToNode)));
  609. }
  610. return addedToNode;
  611. }
  612. void RowAggregateAdapter::PopulateChildren(size_t adapterIndex, const Dom::Value& parentValue, AggregateNode* parentNode)
  613. {
  614. // go through each DOM child of parentValue, ignoring non-rows
  615. const auto numChildren = parentValue.ArraySize();
  616. for (size_t childIndex = 0; childIndex < numChildren; ++childIndex)
  617. {
  618. const auto& childValue = parentValue[childIndex];
  619. // the RowAggregateAdapter groups nodes by row, so we ignore non-child rows here
  620. if (IsRow(childValue))
  621. {
  622. AddChildRow(adapterIndex, parentNode, childValue, childIndex, nullptr);
  623. }
  624. }
  625. }
  626. void RowAggregateAdapter::ProcessRemoval(AggregateNode* rowNode, size_t adapterIndex, Dom::Patch* outgoingPatch)
  627. {
  628. if (NodeIsComplete(rowNode))
  629. {
  630. // value has changed for this node, and it no longer matches its peers.
  631. // If this node was complete before, it isn't now
  632. if (outgoingPatch)
  633. {
  634. auto nodePath = GetPathForNode(rowNode);
  635. if (!nodePath.IsEmpty())
  636. {
  637. outgoingPatch->PushBack(Dom::PatchOperation::RemoveOperation(nodePath));
  638. }
  639. }
  640. }
  641. if (!rowNode->RemoveEntry(adapterIndex))
  642. {
  643. // entry was removed, but node still has entries. Update their "all entries match" status
  644. EvaluateAllEntriesMatch(rowNode);
  645. }
  646. }
  647. void RowAggregateAdapter::UpdateAndPatchNode(
  648. AggregateNode* rowNode,
  649. size_t adapterIndex,
  650. const Dom::PatchOperation& patchOperation,
  651. const Dom::Path& pathPastNode,
  652. Dom::Patch& outgoingPatch)
  653. {
  654. auto& adapter = m_adapters[adapterIndex]->adapter;
  655. auto rowPath = rowNode->GetPathForAdapter(adapterIndex);
  656. const bool nodeWasComplete = NodeIsComplete(rowNode);
  657. auto AddMappedPatchOperation = [&]()
  658. {
  659. auto mappedOperation = patchOperation;
  660. auto operationPath = GetPathForNode(rowNode);
  661. if (!operationPath.IsEmpty())
  662. {
  663. for (size_t pathEntryIndex = 0, numEntries = pathPastNode.Size(); pathEntryIndex < numEntries; ++pathEntryIndex)
  664. {
  665. auto& pathEntry = pathPastNode[pathEntryIndex];
  666. if (pathEntryIndex == 0 && pathEntry.IsIndex())
  667. {
  668. // if the first entry under a row is an index, it must be a column widget. Map its index to its AggregateNode index
  669. const auto targetIndex = pathEntry.GetIndex();
  670. auto sourceRow = adapter->GetContents()[rowPath];
  671. // first determine how many non-row children preceded the target index so that we know what column number we're at
  672. size_t columnIndex = 0;
  673. for (size_t childIndex = 0; childIndex < targetIndex; ++childIndex)
  674. {
  675. if (!IsRow(sourceRow[childIndex]))
  676. {
  677. ++columnIndex;
  678. }
  679. }
  680. // since aggregate nodes always put row DOM values before others, determine how many (complete) rows are
  681. // listed before the column values
  682. size_t completeRowChildren = 0;
  683. for (auto& nodeChild : rowNode->m_childRows)
  684. {
  685. if (NodeIsComplete(nodeChild.get()))
  686. {
  687. ++completeRowChildren;
  688. }
  689. }
  690. // the mapped index is the columnIndex number after all the complete rows are shown. Add that mapped index
  691. operationPath.Push(completeRowChildren + columnIndex);
  692. }
  693. else
  694. {
  695. operationPath.Push(pathEntry);
  696. }
  697. }
  698. mappedOperation.SetDestinationPath(operationPath);
  699. outgoingPatch.PushBack(mappedOperation);
  700. }
  701. };
  702. if (rowNode->EntryCount() > 1)
  703. {
  704. // if there's a node for this value, and it's not the only entry in the node,
  705. // we need to check if it still belongs with the other entries
  706. auto comparisonRow = GetComparisonRow(rowNode, adapterIndex);
  707. AZ_Assert(!comparisonRow.IsNull(), "there should always be a valid comparison value for a node with more than one entry");
  708. // this patch operation is already reflected in the adapter's DOM, retrieve its new value
  709. auto adapterRow = adapter->GetContents()[rowPath];
  710. if (SameRow(comparisonRow, adapterRow))
  711. {
  712. if (m_generateDiffRows)
  713. {
  714. const bool didMatch = rowNode->m_allEntriesMatch;
  715. EvaluateAllEntriesMatch(rowNode);
  716. const bool nowMatches = rowNode->m_allEntriesMatch;
  717. // this node is still is the SameRow so it's as complete as it was before. Check if it changed match status
  718. if (nodeWasComplete && didMatch != nowMatches)
  719. {
  720. outgoingPatch.PushBack(
  721. Dom::PatchOperation::ReplaceOperation(GetPathForNode(rowNode), GetValueHierarchyForNode(rowNode)));
  722. }
  723. }
  724. else
  725. // if this patch was for the "example" (comparison) adapter who provided the aggregate row, then we need to change
  726. // the displayed row accordingly. Map the patch operation's path to the RowAggregateAdapter path, and then add it to
  727. // the outgoing patch
  728. if (nodeWasComplete && rowNode->GetComparisonAdapterIndex() == adapterIndex)
  729. {
  730. AddMappedPatchOperation();
  731. }
  732. }
  733. else
  734. {
  735. // no longer the same row, remove it from the parent and re-add with the new value
  736. // cache off the parent node, since RemoveEntry may well delete ancestorRowNode
  737. auto* parentNode = rowNode->m_parent;
  738. ProcessRemoval(rowNode, adapterIndex, &outgoingPatch);
  739. AddChildRow(adapterIndex, parentNode, adapterRow, rowPath.Back().GetIndex(), &outgoingPatch);
  740. }
  741. }
  742. else
  743. {
  744. // handle case where there's only one entry in this node, but it might've changed.
  745. auto* parentNode = rowNode->m_parent;
  746. // check if this node has changed to match a sibling node
  747. bool matchingSiblingFound = false;
  748. if (parentNode->m_childRows.size() > 1)
  749. {
  750. auto comparisonRow = GetComparisonRow(rowNode);
  751. auto& siblingVector = parentNode->m_childRows;
  752. for (auto siblingIter = siblingVector.begin(), endIter = siblingVector.end();
  753. !matchingSiblingFound && siblingIter != endIter;
  754. ++siblingIter)
  755. {
  756. auto* currSibling = siblingIter->get();
  757. if (currSibling != rowNode)
  758. {
  759. auto siblingRow = GetComparisonRow(currSibling);
  760. if (SameRow(siblingRow, comparisonRow))
  761. {
  762. // now matches sibling, join it as an entry and destroy the former node
  763. ProcessRemoval(rowNode, adapterIndex, &outgoingPatch);
  764. auto adapterRow = adapter->GetContents()[rowPath];
  765. AddChildRow(adapterIndex, parentNode, adapterRow, rowPath.Back().GetIndex(), &outgoingPatch);
  766. matchingSiblingFound = true;
  767. }
  768. }
  769. }
  770. }
  771. const bool nodeIsExampleRow =
  772. (!m_generateDiffRows || rowNode->m_allEntriesMatch) && (rowNode->GetComparisonAdapterIndex() == adapterIndex);
  773. if (!matchingSiblingFound && nodeWasComplete && nodeIsExampleRow)
  774. {
  775. // there's only one entry for this node and it was the shown "example" node,
  776. // add the existing patch operation mapped to the correct path
  777. auto operationPath = GetPathForNode(rowNode);
  778. if (!operationPath.IsEmpty())
  779. {
  780. AddMappedPatchOperation();
  781. }
  782. }
  783. }
  784. }
  785. void RowAggregateAdapter::RemoveChildRows(Dom::Value& rowValue)
  786. {
  787. for (auto valueIter = rowValue.MutableArrayBegin(), endIter = rowValue.MutableArrayEnd(); valueIter != endIter; /*in loop*/)
  788. {
  789. if (IsRow(*valueIter))
  790. {
  791. valueIter = rowValue.ArrayErase(valueIter);
  792. }
  793. else
  794. {
  795. ++valueIter;
  796. }
  797. }
  798. }
  799. Dom::Value RowAggregateAdapter::GenerateContents()
  800. {
  801. m_builder.BeginAdapter();
  802. m_builder.EndAdapter();
  803. Dom::Value contents = m_builder.FinishAndTakeResult();
  804. // root node is not a row, so cannot generate a value directly. Instead, iterate its children for values
  805. for (auto& topLevelRow : m_rootNode->m_childRows)
  806. {
  807. Dom::Value childValue = GetValueHierarchyForNode(topLevelRow.get());
  808. if (!childValue.IsNull())
  809. {
  810. contents.ArrayPushBack(childValue);
  811. }
  812. }
  813. return contents;
  814. }
  815. Dom::Value RowAggregateAdapter::HandleMessage(const AdapterMessage& message)
  816. {
  817. AZ::Dom::Value messageResult;
  818. auto nodePath = message.m_messageOrigin;
  819. auto originalColumn = nodePath.Back().GetIndex();
  820. nodePath.Pop();
  821. auto messageNode = GetNodeAtPath(nodePath);
  822. AZ_Assert(messageNode, "can't find node for given AdapterMessage!");
  823. // check if this is from a "values differ" row
  824. if (!m_generateDiffRows || messageNode->m_allEntriesMatch)
  825. {
  826. /* not a "values differ" row, so it directly represents the member adapter nodes.
  827. Check if this message is one of the ones that the AggregateAdapter is meant to manipulate and forward to sub-adapters */
  828. const auto messagesToForward = GetMessagesToForward();
  829. if (AZStd::find(messagesToForward.begin(), messagesToForward.end(), message.m_messageName) != messagesToForward.end())
  830. {
  831. // it's a forwarded message, we need to look up the original handler for each adapter and call them individually
  832. for (size_t adapterIndex = 0, numAdapters = m_adapters.size(); adapterIndex < numAdapters; ++adapterIndex)
  833. {
  834. auto attributePath = messageNode->GetPathForAdapter(adapterIndex) / originalColumn / message.m_messageName;
  835. auto attributeValue = m_adapters[adapterIndex]->adapter->GetContents()[attributePath];
  836. AZ_Assert(!attributeValue.IsNull(), "function attribute should exist for each adapter!");
  837. auto invokeDomValueFunction = [&message](const Dom::Value& functionValue, auto&& invokeDomValueFunction) -> Dom::Value
  838. {
  839. Dom::Value result;
  840. auto adapterFunction = BoundAdapterMessage::TryMarshalFromDom(functionValue);
  841. if (adapterFunction.has_value())
  842. {
  843. // it's a bound adapter message, just call it, hooray!
  844. result = adapterFunction.value()(message.m_messageParameters);
  845. }
  846. else if (functionValue.IsObject())
  847. {
  848. // it's an object, it should be a callable attribute
  849. auto typeField = functionValue.FindMember(AZ::Attribute::GetTypeField());
  850. if (typeField != functionValue.MemberEnd() && typeField->second.IsString() &&
  851. typeField->second.GetString() == Attribute::GetTypeName())
  852. {
  853. // last chance! Check if it's an invokable Attribute
  854. AZ::Attribute* attribute =
  855. AZ::Dom::Utils::ValueToTypeUnsafe<AZ::Attribute*>(functionValue[AZ::Attribute::GetAttributeField()]);
  856. AZ::Dom::Value instanceAndArgs(AZ::Dom::Type::Array);
  857. instanceAndArgs.ArrayPushBack(functionValue[AZ::Attribute::GetInstanceField()]);
  858. instanceAndArgs.ArrayInsert(
  859. instanceAndArgs.ArrayEnd(),
  860. message.m_messageParameters.ArrayBegin(),
  861. message.m_messageParameters.ArrayEnd());
  862. const bool canInvoke = attribute->IsInvokable() && attribute->CanDomInvoke(instanceAndArgs);
  863. AZ_Assert(canInvoke, "message attribute is not invokable!");
  864. if (canInvoke)
  865. {
  866. result = attribute->DomInvoke(instanceAndArgs);
  867. }
  868. }
  869. }
  870. else if (functionValue.IsArray())
  871. {
  872. for (auto arrayIter = functionValue.ArrayBegin(), endIter = functionValue.ArrayEnd(); arrayIter != endIter;
  873. ++arrayIter)
  874. {
  875. // Note: currently last call in the array wins. This could be parameterized in the future if
  876. // a different result is desired
  877. result = invokeDomValueFunction(*arrayIter, invokeDomValueFunction);
  878. }
  879. }
  880. else
  881. {
  882. // it's not a function object, it's most likely a pass-through Value, so pass it through
  883. result = functionValue;
  884. }
  885. return result;
  886. };
  887. messageResult = invokeDomValueFunction(attributeValue, invokeDomValueFunction);
  888. }
  889. }
  890. }
  891. else
  892. {
  893. // not a member-adapter generated message
  894. auto handleEditAnyway = [&]()
  895. {
  896. // get the affected row by pulling off the trailing column index on the address
  897. auto rowPath = message.m_messageOrigin;
  898. rowPath.Pop();
  899. /* edit anyway forces us to act as if one representative node can talk to all sub-adapters, so from this point forward
  900. we are effectively pretending that this row now matches all sub-adapters */
  901. messageNode->m_allEntriesMatch = true;
  902. NotifyContentsChanged({ Dom::PatchOperation::ReplaceOperation(rowPath, GenerateAggregateRow(GetNodeAtPath(rowPath))) });
  903. };
  904. messageResult = message.Match(Nodes::GenericButton::OnActivate, handleEditAnyway);
  905. }
  906. return messageResult;
  907. }
  908. AZStd::string_view LabeledRowAggregateAdapter::GetFirstLabel(const Dom::Value& rowValue)
  909. {
  910. for (auto arrayIter = rowValue.ArrayBegin(), endIter = rowValue.ArrayEnd(); arrayIter != endIter; ++arrayIter)
  911. {
  912. auto& currChild = *arrayIter;
  913. if (arrayIter->GetNodeName() == AZ::Dpe::GetNodeName<AZ::Dpe::Nodes::Label>())
  914. {
  915. return AZ::Dpe::Nodes::Label::Value.ExtractFromDomNode(currChild).value_or("");
  916. }
  917. }
  918. return AZStd::string_view();
  919. }
  920. AZStd::vector<AZ::Name> LabeledRowAggregateAdapter::GetMessagesToForward()
  921. {
  922. return { Nodes::PropertyEditor::OnChanged.GetName(),
  923. Nodes::PropertyEditor::ChangeNotify.GetName(),
  924. Nodes::PropertyEditor::ChangeValidate.GetName(),
  925. Nodes::PropertyEditor::RequestTreeUpdate.GetName(),
  926. Nodes::GenericButton::OnActivate.GetName() };
  927. }
  928. Dom::Value LabeledRowAggregateAdapter::GenerateAggregateRow(AggregateNode* matchingNode)
  929. {
  930. /* use the row generated by the first matching adapter as a template, but replace its message
  931. handlers with a redirect to our own adapter. These will then be forwarded as needed in
  932. RowAggregateAdapter::HandleMessage */
  933. auto multiRow = GetComparisonRow(matchingNode);
  934. const auto editorName = AZ::Dpe::GetNodeName<AZ::Dpe::Nodes::PropertyEditor>();
  935. const auto messagesToForward = GetMessagesToForward();
  936. for (size_t childIndex = 0, numChildren = multiRow.ArraySize(); childIndex < numChildren; ++childIndex)
  937. {
  938. auto& childValue = multiRow.MutableArrayAt(childIndex);
  939. if (childValue.GetNodeName() == editorName)
  940. {
  941. for (auto attributeIter = childValue.MutableMemberBegin(), attributeEnd = childValue.MutableMemberEnd();
  942. attributeIter != attributeEnd;
  943. ++attributeIter)
  944. {
  945. for (const auto& messageName : messagesToForward)
  946. {
  947. // check if the adapter wants this message forwarded, and replace it with our own handler if it does
  948. if (attributeIter->first == messageName)
  949. {
  950. auto nodePath = GetPathForNode(matchingNode);
  951. AZ_Assert(!nodePath.IsEmpty(), "shouldn't be generating an aggregate row for a non-matching node!");
  952. BoundAdapterMessage changedAttribute = { this, messageName, nodePath / childIndex, {} };
  953. auto newValue = changedAttribute.MarshalToDom();
  954. attributeIter->second = newValue;
  955. // we've found the matching message, break out of the inner loop
  956. break;
  957. }
  958. }
  959. }
  960. }
  961. }
  962. return multiRow;
  963. }
  964. Dom::Value LabeledRowAggregateAdapter::GenerateValuesDifferRow([[maybe_unused]] AggregateNode* mismatchNode)
  965. {
  966. m_builder.SetCurrentPath(GetPathForNode(mismatchNode));
  967. m_builder.BeginRow();
  968. m_builder.Label(GetFirstLabel(GetComparisonRow(mismatchNode)));
  969. m_builder.Label(AZStd::string("Values Differ"));
  970. m_builder.BeginPropertyEditor<Nodes::GenericButton>();
  971. m_builder.Attribute(Nodes::PropertyEditor::SharePriorColumn, true);
  972. m_builder.Attribute(Nodes::Button::ButtonText, AZStd::string("Edit Anyway"));
  973. m_builder.AddMessageHandler(this, Nodes::GenericButton::OnActivate.GetName());
  974. m_builder.EndPropertyEditor();
  975. m_builder.EndRow();
  976. return m_builder.FinishAndTakeResult();
  977. }
  978. bool LabeledRowAggregateAdapter::SameRow(const Dom::Value& newRow, const Dom::Value& existingRow)
  979. {
  980. auto newNodeText = GetFirstLabel(newRow);
  981. auto existingNodeText = GetFirstLabel(existingRow);
  982. return (newNodeText == existingNodeText);
  983. }
  984. bool LabeledRowAggregateAdapter::ValuesMatch(const Dom::Value& left, const Dom::Value& right)
  985. {
  986. auto getComparisonValue = [](const Dom::Value& rowValue)
  987. {
  988. if (!rowValue.IsArrayEmpty())
  989. {
  990. for (auto arrayIter = rowValue.ArrayBegin() + 1, endIter = rowValue.ArrayEnd(); arrayIter != endIter; ++arrayIter)
  991. {
  992. auto& currChild = *arrayIter;
  993. if (arrayIter->GetNodeName() == AZ::Dpe::GetNodeName<AZ::Dpe::Nodes::PropertyEditor>())
  994. {
  995. return AZ::Dpe::Nodes::PropertyEditor::Value.ExtractFromDomNode(currChild).value_or(
  996. Dom::Value(AZ::Dom::Type::Null));
  997. }
  998. }
  999. }
  1000. return Dom::Value(AZ::Dom::Type::Null);
  1001. };
  1002. auto leftValue = getComparisonValue(left);
  1003. auto rightValue = getComparisonValue(right);
  1004. return (leftValue.GetType() != AZ::Dom::Type::Null && leftValue == rightValue);
  1005. }
  1006. } // namespace AZ::DocumentPropertyEditor