3
0

CommandLine.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  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 "CommandLine.h"
  9. #include <QString>
  10. #include <QKeyEvent>
  11. #include <QCompleter>
  12. #include <QGraphicsItem>
  13. #include <QGraphicsView>
  14. #include "Editor/View/Widgets/ui_CommandLine.h"
  15. #include <AzCore/Component/ComponentApplicationBus.h>
  16. #include <AzCore/Serialization/SerializeContext.h>
  17. #include <AzCore/Serialization/EditContext.h>
  18. #include <Core/Attributes.h>
  19. #include <Core/Node.h>
  20. #include <AzFramework/StringFunc/StringFunc.h>
  21. #include <Editor/Nodes/NodeCreateUtils.h>
  22. #include <Editor/QtMetaTypes.h>
  23. #include <ScriptCanvas/Bus/RequestBus.h>
  24. #include <GraphCanvas/Components/SceneBus.h>
  25. #include <GraphCanvas/Components/ViewBus.h>
  26. #include <GraphCanvas/Components/GeometryBus.h>
  27. namespace
  28. {
  29. using namespace ScriptCanvasEditor;
  30. using namespace ScriptCanvasEditor::Widget;
  31. void CreateSelectedNodes(CommandLine* commandLine,
  32. AZStd::unique_ptr<Ui::CommandLine>& ui,
  33. AZ::SerializeContext* serializeContext)
  34. {
  35. if (!ui->commandList->selectionModel())
  36. {
  37. return;
  38. }
  39. if (ui->commandList->selectionModel()->selectedIndexes().empty())
  40. {
  41. // Nothing selected.
  42. return;
  43. }
  44. CommandListDataProxyModel* dataModel = qobject_cast<CommandListDataProxyModel*>(ui->commandList->model());
  45. if (!dataModel)
  46. {
  47. return;
  48. }
  49. ScriptCanvas::ScriptCanvasId scriptCanvasId;
  50. ScriptCanvasEditor::GeneralRequestBus::BroadcastResult(scriptCanvasId, &ScriptCanvasEditor::GeneralRequests::GetActiveScriptCanvasId);
  51. AZ::EntityId graphCanvasGraphId;
  52. ScriptCanvasEditor::GeneralRequestBus::BroadcastResult(graphCanvasGraphId, &ScriptCanvasEditor::GeneralRequests::GetActiveGraphCanvasGraphId);
  53. if (!(scriptCanvasId.IsValid() &&
  54. graphCanvasGraphId.IsValid()))
  55. {
  56. // Nothing active.
  57. return;
  58. }
  59. // Create the nodes in a horizontal list at the top of the canvas.
  60. AZ::Vector2 pos(20.0f, 20.0f);
  61. for (const auto& index : ui->commandList->selectionModel()->selectedIndexes())
  62. {
  63. if (index.column() != CommandListDataModel::ColumnIndex::CommandIndex)
  64. {
  65. continue;
  66. }
  67. AZ::Uuid type = dataModel->data(index, CommandListDataModel::CustomRole::Types).value<AZ::Uuid>();
  68. if (type.IsNull())
  69. {
  70. continue;
  71. }
  72. [[maybe_unused]] const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(type);
  73. AZ_Assert(classData, "Failed to find ClassData for ID: %s", type.ToString<AZStd::string>().data());
  74. ScriptCanvasEditor::Nodes::StyleConfiguration styleConfiguration;
  75. NodeIdPair nodePair = ScriptCanvasEditor::Nodes::CreateNode(type, scriptCanvasId, styleConfiguration);
  76. GraphCanvas::SceneRequestBus::Event(graphCanvasGraphId, &GraphCanvas::SceneRequests::AddNode, nodePair.m_graphCanvasId, pos, false);
  77. // The next position to create a node at.
  78. // IMPORTANT: This SHOULD be using GraphCanvas::GeometryRequests::GetWidth, but
  79. // GetWidth is currently returning zero, so we'll TEMPORARILY use a fixed value.
  80. pos += AZ::Vector2(125.0f, 0.0f);
  81. }
  82. commandLine->hide();
  83. }
  84. } // anonymous namespace.
  85. namespace ScriptCanvasEditor
  86. {
  87. namespace Widget
  88. {
  89. // CommandListDataModel
  90. /////////////////////////////////////////////////////////////////////////////////////////////
  91. CommandListDataModel::CommandListDataModel([[maybe_unused]] QWidget* parent /*= nullptr*/)
  92. {
  93. ScriptCanvasCommandLineRequestBus::Handler::BusConnect();
  94. AZ::SerializeContext* serializeContext = nullptr;
  95. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
  96. serializeContext->EnumerateDerived<ScriptCanvas::Node>(
  97. [this](const AZ::SerializeContext::ClassData* classData, [[maybe_unused]] const AZ::Uuid& classUuid) -> bool
  98. {
  99. if (classData && classData->m_editData)
  100. {
  101. bool add = true;
  102. if (auto editorElementData = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData))
  103. {
  104. if (auto excludeAttribute = editorElementData->FindAttribute(AZ::Script::Attributes::ExcludeFrom))
  105. {
  106. auto excludeAttributeData = azdynamic_cast<const AZ::Edit::AttributeData<bool>*>(excludeAttribute);
  107. if (excludeAttributeData)
  108. {
  109. add = false;
  110. }
  111. }
  112. }
  113. if (add)
  114. {
  115. Entry entry;
  116. entry.m_type = classData->m_typeId;
  117. m_entries.emplace_back(entry);
  118. }
  119. }
  120. return true;
  121. }
  122. );
  123. ScriptCanvasCommandLineRequestBus::Broadcast(&ScriptCanvasCommandLineRequests::AddCommand, "add_node", "Adds the specified node to the graph",
  124. [serializeContext](const AZStd::vector<AZStd::string>& nodes)
  125. {
  126. AZ::Uuid nodeTypeToAdd = AZ::Uuid::CreateNull();
  127. if (nodes.size() > 0)
  128. {
  129. const AZStd::string& nodeName = *(nodes.begin());
  130. serializeContext->EnumerateDerived<ScriptCanvas::Node>(
  131. [&nodeName, &nodeTypeToAdd](const AZ::SerializeContext::ClassData* classData, [[maybe_unused]] const AZ::Uuid& classUuid) -> bool
  132. {
  133. if (classData && classData->m_editData)
  134. {
  135. if (nodeName.compare(classData->m_name) == 0)
  136. {
  137. nodeTypeToAdd = classData->m_typeId;
  138. }
  139. }
  140. return true;
  141. }
  142. );
  143. if (!nodeTypeToAdd.IsNull())
  144. {
  145. ScriptCanvas::ScriptCanvasId scriptCanvasId;
  146. ScriptCanvasEditor::GeneralRequestBus::BroadcastResult(scriptCanvasId, &ScriptCanvasEditor::GeneralRequests::GetActiveScriptCanvasId);
  147. AZ::EntityId graphCanvasGraphId;
  148. ScriptCanvasEditor::GeneralRequestBus::BroadcastResult(graphCanvasGraphId, &ScriptCanvasEditor::GeneralRequests::GetActiveGraphCanvasGraphId);
  149. if (scriptCanvasId.IsValid() && graphCanvasGraphId.IsValid())
  150. {
  151. ScriptCanvasEditor::Nodes::StyleConfiguration styleConfiguration;
  152. AZ::Vector2 pos(100.0f, 20.0f);
  153. NodeIdPair nodePair = ScriptCanvasEditor::Nodes::CreateNode(nodeTypeToAdd, scriptCanvasId, styleConfiguration);
  154. GraphCanvas::SceneRequestBus::Event(graphCanvasGraphId, &GraphCanvas::SceneRequests::AddNode, nodePair.m_graphCanvasId, pos, false);
  155. }
  156. }
  157. }
  158. }
  159. );
  160. }
  161. CommandListDataModel::~CommandListDataModel()
  162. {
  163. ScriptCanvasCommandLineRequestBus::Handler::BusDisconnect();
  164. }
  165. QModelIndex CommandListDataModel::index(int row, int column, const QModelIndex& parent /*= QModelIndex()*/) const
  166. {
  167. if (row >= rowCount(parent) || column >= columnCount(parent))
  168. {
  169. return QModelIndex();
  170. }
  171. return createIndex(row, column);
  172. }
  173. QModelIndex CommandListDataModel::parent([[maybe_unused]] const QModelIndex& child) const
  174. {
  175. return QModelIndex();
  176. }
  177. int CommandListDataModel::rowCount([[maybe_unused]] const QModelIndex& parent /*= QModelIndex()*/) const
  178. {
  179. return static_cast<int>(m_entries.size());
  180. }
  181. int CommandListDataModel::columnCount([[maybe_unused]] const QModelIndex& parent /*= QModelIndex()*/) const
  182. {
  183. return ColumnIndex::Count;
  184. }
  185. QVariant CommandListDataModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const
  186. {
  187. AZ::SerializeContext* serializeContext = nullptr;
  188. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
  189. if (role == Qt::DisplayRole)
  190. {
  191. if (index.row() == 0)
  192. {
  193. if (index.column() == 0)
  194. {
  195. return QVariant(QString(tr("No results found.")));
  196. }
  197. else
  198. if (index.column() > 0)
  199. {
  200. return QVariant();
  201. }
  202. }
  203. AZ::Uuid nodeType = m_entries[index.row()].m_type;
  204. if (nodeType.IsNull())
  205. {
  206. if (index.column() == ColumnIndex::CommandIndex)
  207. {
  208. return QVariant(QString(m_entries[index.row()].m_command.c_str()));
  209. }
  210. if (index.column() == ColumnIndex::DescriptionIndex)
  211. {
  212. AZStd::string command = m_entries[index.row()].m_command;
  213. const auto& entry = m_commands.find(command);
  214. if (entry != m_commands.end())
  215. {
  216. return QVariant(QString(entry->second->GetDescription().c_str()));
  217. }
  218. }
  219. }
  220. else
  221. {
  222. if (const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(nodeType))
  223. {
  224. if (index.column() == ColumnIndex::CommandIndex)
  225. {
  226. return QVariant(QString(classData->m_name));
  227. }
  228. if (index.column() == ColumnIndex::DescriptionIndex)
  229. {
  230. return QVariant(QString(classData->m_editData ? classData->m_editData->m_description : tr("No description provided.")));
  231. }
  232. if (index.column() == ColumnIndex::TrailIndex)
  233. {
  234. return QVariant(QString(""));
  235. }
  236. }
  237. }
  238. }
  239. switch (role)
  240. {
  241. case CustomRole::Types:
  242. {
  243. AZ::Uuid nodeType = m_entries[index.row()].m_type;
  244. return QVariant::fromValue<AZ::Uuid>(nodeType);
  245. }
  246. break;
  247. case CustomRole::Node:
  248. {
  249. AZ::Uuid nodeType = m_entries[index.row()].m_type;
  250. if (nodeType.IsNull())
  251. {
  252. return QVariant(QString(m_entries[index.row()].m_command.c_str()));
  253. }
  254. else
  255. {
  256. if (const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(nodeType))
  257. {
  258. if (index.column() == ColumnIndex::CommandIndex)
  259. {
  260. return QVariant(QString(classData->m_name));
  261. }
  262. if (index.column() == ColumnIndex::DescriptionIndex)
  263. {
  264. return QVariant(QString(classData->m_editData ? classData->m_editData->m_description : tr("No description provided.")));
  265. }
  266. if (index.column() == ColumnIndex::TrailIndex)
  267. {
  268. return QVariant(QString(""));
  269. }
  270. }
  271. }
  272. }
  273. break;
  274. case CustomRole::Commands:
  275. {
  276. if (index.column() == ColumnIndex::CommandIndex)
  277. {
  278. return QVariant(QString(m_entries[index.row()].m_command.c_str()));
  279. }
  280. }
  281. break;
  282. default:
  283. break;
  284. }
  285. return QVariant();
  286. }
  287. Qt::ItemFlags CommandListDataModel::flags(const QModelIndex& index) const
  288. {
  289. return QAbstractTableModel::flags(index);
  290. }
  291. bool CommandListDataModel::HasMatches(const AZStd::string& input)
  292. {
  293. AZ::SerializeContext* serializeContext = nullptr;
  294. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
  295. for (const auto& entry : m_entries)
  296. {
  297. if (!entry.m_type.IsNull())
  298. {
  299. if (const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(entry.m_type))
  300. {
  301. QString name = QString(classData->m_name);
  302. if (name.startsWith(input.c_str(), Qt::CaseSensitivity::CaseInsensitive))
  303. {
  304. return true;
  305. }
  306. }
  307. }
  308. else
  309. {
  310. QString commandName = entry.m_command.c_str();
  311. return (commandName.startsWith(input.c_str(), Qt::CaseSensitivity::CaseInsensitive));
  312. }
  313. }
  314. return false;
  315. }
  316. ScriptCanvasEditor::Widget::CommandRegistry CommandListDataModel::m_commands;
  317. // CommandLineEdit
  318. /////////////////////////////////////////////////////////////////////////////////////////////
  319. namespace
  320. {
  321. QString s_defaultText = QStringLiteral("Press ? for help");
  322. }
  323. CommandLineEdit::CommandLineEdit(QWidget* parent /*= nullptr*/)
  324. : QLineEdit(parent)
  325. , m_empty(true)
  326. , m_defaultText(s_defaultText)
  327. {
  328. ResetState();
  329. connect(this, &QLineEdit::textChanged, this, &CommandLineEdit::onTextChanged);
  330. connect(this, &QLineEdit::textEdited, this, &CommandLineEdit::onTextEdited);
  331. connect(this, &QLineEdit::returnPressed, this, &CommandLineEdit::onReturnPressed);
  332. }
  333. void CommandLineEdit::onReturnPressed()
  334. {
  335. }
  336. void CommandLineEdit::onTextChanged([[maybe_unused]] const QString& text)
  337. {
  338. }
  339. void CommandLineEdit::onTextEdited(const QString& text)
  340. {
  341. // Execute the command
  342. (void)text;
  343. if (text.isEmpty())
  344. {
  345. ResetState();
  346. }
  347. }
  348. void CommandLineEdit::focusInEvent(QFocusEvent* e)
  349. {
  350. QLineEdit::focusInEvent(e);
  351. Q_EMIT(onFocusChange(true));
  352. }
  353. void CommandLineEdit::focusOutEvent(QFocusEvent* e)
  354. {
  355. QLineEdit::focusInEvent(e);
  356. Q_EMIT(onFocusChange(true));
  357. }
  358. void CommandLineEdit::ResetState()
  359. {
  360. m_empty = true;
  361. setText(m_defaultText);
  362. }
  363. void CommandLineEdit::keyReleaseEvent(QKeyEvent* event)
  364. {
  365. Q_EMIT(onKeyReleased(event));
  366. }
  367. void CommandLineEdit::keyPressEvent(QKeyEvent* event)
  368. {
  369. switch (event->key())
  370. {
  371. case Qt::Key_Enter:
  372. case Qt::Key_Return:
  373. {
  374. // Invoke the command
  375. AZStd::string commandText = text().toStdString().c_str();
  376. AZStd::vector<AZStd::string> tokens;
  377. AZ::StringFunc::Tokenize(commandText, tokens, " ");
  378. if (tokens.size() == 1)
  379. {
  380. ScriptCanvasCommandLineRequestBus::Broadcast(&ScriptCanvasCommandLineRequests::Invoke, tokens.begin()->c_str());
  381. }
  382. else if (tokens.size() > 1)
  383. {
  384. AZStd::string command = *(tokens.begin());
  385. AZStd::vector<AZStd::string> args;
  386. for (auto it = tokens.begin() + 1; it != tokens.end(); ++it)
  387. {
  388. args.push_back(*it);
  389. }
  390. ScriptCanvasCommandLineRequestBus::Broadcast(&ScriptCanvasCommandLineRequests::InvokeWithArguments, command.c_str(), args);
  391. }
  392. ResetState();
  393. qobject_cast<QWidget*>(parent())->hide();
  394. }
  395. break;
  396. case Qt::Key_Backspace:
  397. if (m_empty)
  398. {
  399. break;
  400. }
  401. QLineEdit::keyPressEvent(event);
  402. break;
  403. case Qt::Key_Escape:
  404. ResetState();
  405. qobject_cast<QWidget*>(parent())->hide();
  406. return;
  407. default:
  408. if (m_empty)
  409. {
  410. setText("");
  411. m_empty = false;
  412. }
  413. QLineEdit::keyPressEvent(event);
  414. break;
  415. }
  416. }
  417. // CommandLineList
  418. /////////////////////////////////////////////////////////////////////////////////////////////
  419. CommandLineList::CommandLineList(QWidget* parent /*= nullptr*/)
  420. : QTableView(parent)
  421. {
  422. }
  423. // CommandListDataProxyModel
  424. /////////////////////////////////////////////////////////////////////////////////////////////
  425. CommandListDataProxyModel::CommandListDataProxyModel(CommandListDataModel* commandListData, QObject* parent /*= nullptr*/)
  426. : QSortFilterProxyModel(parent)
  427. {
  428. setSourceModel(commandListData);
  429. QStringList commandList;
  430. for (int i = 0; i < commandListData->rowCount(); ++i)
  431. {
  432. QModelIndex index = commandListData->index(i, CommandListDataModel::ColumnIndex::CommandIndex);
  433. QString command = commandListData->data(index, CommandListDataModel::CustomRole::Node).toString();
  434. commandList.push_back(command);
  435. }
  436. ScriptCanvasCommandLineRequests::CommandNameList commands;
  437. ScriptCanvasCommandLineRequestBus::BroadcastResult(commands, &ScriptCanvasCommandLineRequests::GetCommands);
  438. for (auto& command : commands)
  439. {
  440. QString commandName = command.first.c_str();
  441. commandList.push_back(commandName);
  442. }
  443. m_completer = new QCompleter(commandList);
  444. m_completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
  445. m_completer->setCaseSensitivity(Qt::CaseInsensitive);
  446. }
  447. bool CommandListDataProxyModel::filterAcceptsRow(int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
  448. {
  449. CommandListDataModel* dataModel = qobject_cast<CommandListDataModel*>(sourceModel());
  450. if (sourceRow < 0 || sourceRow >= dataModel->rowCount())
  451. {
  452. return false;
  453. }
  454. if (m_input.empty() || m_input.compare(s_defaultText.toStdString().c_str()) == 0)
  455. {
  456. return false;
  457. }
  458. if (AzFramework::StringFunc::FirstCharacter(m_input.c_str()) == '?')
  459. {
  460. if (sourceRow > 0)
  461. {
  462. return true;
  463. }
  464. else
  465. {
  466. return false;
  467. }
  468. }
  469. QModelIndex index = dataModel->index(sourceRow, CommandListDataModel::ColumnIndex::CommandIndex);
  470. QString sourceStr = dataModel->data(index).toString();
  471. if (sourceRow > 0 && sourceStr.startsWith(m_input.c_str(), Qt::CaseSensitivity::CaseInsensitive))
  472. {
  473. return true;
  474. }
  475. else
  476. if (sourceRow == 0)
  477. {
  478. if (!dataModel->HasMatches(m_input))
  479. {
  480. return true;
  481. }
  482. }
  483. return false;
  484. }
  485. // CommandLine
  486. /////////////////////////////////////////////////////////////////////////////////////////////
  487. CommandLine::CommandLine(QWidget* parent /*= nullptr*/)
  488. : QWidget(parent)
  489. , ui(new Ui::CommandLine())
  490. {
  491. ui->setupUi(this);
  492. CommandListDataModel* commandListDataModel = new CommandListDataModel();
  493. CommandListDataProxyModel* commandListDataProxyModel = new CommandListDataProxyModel(commandListDataModel);
  494. ui->commandList->setModel(commandListDataProxyModel);
  495. connect(ui->commandText, &QLineEdit::textChanged, this, &CommandLine::onTextChanged);
  496. connect(ui->commandText, &CommandLineEdit::onKeyReleased, this, &CommandLine::onEditKeyReleaseEvent);
  497. connect(ui->commandList, &CommandLineList::onKeyReleased, this, &CommandLine::onListKeyReleaseEvent);
  498. ui->commandList->setColumnWidth(CommandListDataModel::ColumnIndex::CommandIndex, 250);
  499. ui->commandList->setColumnWidth(CommandListDataModel::ColumnIndex::DescriptionIndex, 1000);
  500. }
  501. void CommandLine::onTextChanged(const QString& text)
  502. {
  503. CommandListDataProxyModel* model = qobject_cast<CommandListDataProxyModel*>(ui->commandList->model());
  504. if (model)
  505. {
  506. model->SetInput(text.toStdString().c_str());
  507. }
  508. }
  509. void CommandLine::onListKeyReleaseEvent(QKeyEvent* event)
  510. {
  511. AZ::SerializeContext* serializeContext = nullptr;
  512. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
  513. AZ_Assert(serializeContext, "Failed to acquire application serialize context.");
  514. switch (event->key())
  515. {
  516. case Qt::Key_Up:
  517. if (ui->commandList->selectionModel())
  518. {
  519. if (ui->commandList->selectionModel()->selectedIndexes().empty() || ui->commandList->selectionModel()->selectedIndexes().at(0).row() == 0)
  520. {
  521. ui->commandText->setFocus();
  522. }
  523. }
  524. break;
  525. case Qt::Key_Escape:
  526. hide();
  527. break;
  528. case Qt::Key_Enter:
  529. case Qt::Key_Return:
  530. CreateSelectedNodes(this, ui, serializeContext);
  531. break;
  532. }
  533. }
  534. void CommandLine::onEditKeyReleaseEvent(QKeyEvent* event)
  535. {
  536. if (event->key() == Qt::Key_Down)
  537. {
  538. ui->commandList->setFocus();
  539. ui->commandList->selectRow(0);
  540. }
  541. }
  542. void CommandLine::showEvent(QShowEvent* event)
  543. {
  544. QWidget::showEvent(event);
  545. ui->commandText->ResetState();
  546. ui->commandText->setFocus(Qt::PopupFocusReason);
  547. }
  548. #include <Editor/View/Widgets/moc_CommandLine.cpp>
  549. }
  550. }