/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") #include AZ_POP_DISABLE_WARNING #include #include #include #include #include #include #include #include namespace ScriptCanvasEditor { //////////////////// // ContainerWizard //////////////////// ContainerWizard::ContainerWizard(QWidget* parent) : QDialog(parent, Qt::FramelessWindowHint) , m_serializeContext(nullptr) , m_validationAction(nullptr) , m_invalidIcon(":/ScriptCanvasEditorResources/Resources/error_icon.png") , m_dataTypeMenuVisibile(false) , m_releaseVariable(false) , m_variableCounter(0) , m_ui(new Ui::ContainerWizard()) { m_ui->setupUi(this); QObject::connect(m_ui->containerTypeBox, static_cast(&QComboBox::currentIndexChanged), this, &ContainerWizard::OnContainerTypeChanged); m_ui->containerTypeBox->setEditable(false); // Don't want to enable enter triggering through the default mechanism for the create/cancel, since it causes a bunch of accidental triggers // of submission while editing. Instead install an event filter and deal with this internally. m_ui->createButton->setFocusPolicy(Qt::FocusPolicy::StrongFocus); m_ui->createButton->setAutoDefault(false); m_ui->createButton->setDefault(false); m_ui->createButton->installEventFilter(this); m_ui->cancelButton->setFocusPolicy(Qt::FocusPolicy::StrongFocus); m_ui->cancelButton->setAutoDefault(false); m_ui->cancelButton->setDefault(false); m_ui->cancelButton->installEventFilter(this); QObject::connect(m_ui->createButton, &QPushButton::clicked, this, &ContainerWizard::OnCreate); QObject::connect(m_ui->cancelButton, &QPushButton::clicked, this, &ContainerWizard::OnCancel); QObject::connect(this, &QDialog::finished, this, &ContainerWizard::OnFinished); QObject::connect(m_ui->variableName, &QLineEdit::textChanged, this, &ContainerWizard::ValidateName); } ContainerWizard::~ContainerWizard() { ClearDisplay(); for (ContainerTypeLineEdit* lineEdit : m_containerTypeLineEdit) { delete lineEdit; } } void ContainerWizard::SetActiveScriptCanvasId(const ScriptCanvas::ScriptCanvasId& scriptCanvasId) { m_activeScriptCanvasId = scriptCanvasId; } void ContainerWizard::RegisterType(const AZ::TypeId& dataType) { if (AZ::Utils::IsContainerType(dataType)) { RegisterContainerType(dataType); } else { RegisterDataType(dataType); } } void ContainerWizard::ShowWizard(const AZ::TypeId& genericContainerType) { // Always default the wizard to the unchecked state m_ui->checkBox->setChecked(false); if (m_ui->containerTypeBox->count() != m_genericContainerTypes.size()) { QSignalBlocker signalBlock(m_ui->containerTypeBox); std::sort(m_genericContainerTypeNames.begin(), m_genericContainerTypeNames.end(), [](const AZStd::pair< AZStd::string, AZ::TypeId>& lhs, const AZStd::pair< AZStd::string, AZ::TypeId>& rhs) { return AZStd::less()(lhs.first, rhs.first); }); m_ui->containerTypeBox->clear(); for (const auto& element : m_genericContainerTypeNames) { m_ui->containerTypeBox->addItem(element.first.c_str()); } } for (int i = 0; i < m_genericContainerTypeNames.size(); ++i) { const auto& element = m_genericContainerTypeNames[i]; if (element.second == genericContainerType) { QSignalBlocker signalBlock(m_ui->containerTypeBox); m_ui->containerTypeBox->setCurrentIndex(i); } } // Need to show before trying to initialize the display otherwise the line edits won't be cleaned up // correctly. show(); InitializeDisplay(genericContainerType); m_releaseVariable = true; bool nameAvailable = false; AZStd::string variableName; do { SceneCounterRequestBus::EventResult(m_variableCounter, m_activeScriptCanvasId, &SceneCounterRequests::GetNewVariableCounter); variableName = VariableDockWidget::ConstructDefaultVariableName(m_variableCounter); ScriptCanvas::GraphVariableManagerRequestBus::EventResult(nameAvailable, m_activeScriptCanvasId, &ScriptCanvas::GraphVariableManagerRequests::IsNameAvailable, variableName); } while (!nameAvailable); m_ui->variableName->setText(variableName.c_str()); m_ui->variableName->setFocus(Qt::FocusReason::MouseFocusReason); m_ui->variableName->setSelection(0, m_ui->variableName->text().size()); } void ContainerWizard::accept() { QDialog::accept(); } void ContainerWizard::reject() { if (m_dataTypeMenuVisibile) { for (ContainerTypeLineEdit* lineEdit : m_containerTypeLineEdit) { lineEdit->HideDataTypeMenu(); } m_dataTypeMenuVisibile = false; } else { QDialog::reject(); } } void ContainerWizard::hideEvent(QHideEvent* hideEvent) { QDialog::hideEvent(hideEvent); for (ContainerTypeLineEdit* lineEdit : m_containerTypeLineEdit) { lineEdit->HideDataTypeMenu(); } m_dataTypeMenuVisibile = false; } bool ContainerWizard::eventFilter(QObject* object, QEvent* event) { if (object == m_ui->createButton || object == m_ui->cancelButton) { if (event->type() == QEvent::Type::KeyRelease) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { QPushButton* button = qobject_cast(object); if (button) { button->click(); } } } } return false; } const AZStd::unordered_map& ContainerWizard::GetFinalTypeMapping() const { return m_finalContainerTypeIds; } void ContainerWizard::ReparseDisplay() { AZ::Crc32 workingCrc = AZ::Crc32(m_genericType.ToString().c_str()); for (int typeIndex = 0; typeIndex < m_containerTypes.size(); ++typeIndex) { const AZ::TypeId& typeId = m_containerTypes[typeIndex]; ContainerTypeLineEdit* lineEdit = GetLineEdit(typeIndex); auto dataTypeIter = m_containerDataTypeSets.find(workingCrc); if (dataTypeIter == m_containerDataTypeSets.end()) { // No idea wtf do here we've managed to put ourselves into an invalid state AZ_Error("ScriptCanvas", false, "Unknown partial type found in Container Creation. Aborting."); close(); break; } lineEdit->SetDataTypes(dataTypeIter->second); AZ::TypeId selectedTypeId = typeId; { QSignalBlocker signalBlocker(lineEdit); if (!lineEdit->DisplayType(typeId)) { selectedTypeId = lineEdit->GetDefaultTypeId(); lineEdit->DisplayType(typeId); } } m_containerTypes[typeIndex] = selectedTypeId; workingCrc.Add(selectedTypeId.ToString().c_str()); } } void ContainerWizard::OnCreate() { AZ::Crc32 typeCrc = AZ::Crc32(m_genericType.ToString().c_str()); for (const AZ::TypeId& typeId : m_containerTypes) { typeCrc.Add(typeId.ToString().c_str()); } auto containerIter = m_finalContainerTypeIds.find(typeCrc); if (containerIter != m_finalContainerTypeIds.end()) { m_releaseVariable = false; AZStd::string variableName = m_ui->variableName->text().toUtf8().data(); if (m_ui->checkBox->isChecked()) { AZStd::intrusive_ptr settings = AZ::UserSettings::CreateFind(AZ_CRC("ScriptCanvasPreviewSettings", 0x1c5a2965), AZ::UserSettings::CT_LOCAL); if (settings) { auto insertResult = settings->m_pinnedDataTypes.insert(containerIter->second); if (insertResult.second) { Q_EMIT ContainerPinned(containerIter->second); } } } Q_EMIT CreateContainerVariable(variableName, containerIter->second); close(); } else { AZ_Warning("ScriptCanvas", false, "Unable to find Registered type with the given parameters."); } } void ContainerWizard::OnCancel() { close(); } void ContainerWizard::OnFinished(int result) { ClearDisplay(); if (m_releaseVariable) { SceneCounterRequestBus::Event(m_activeScriptCanvasId, &SceneCounterRequests::ReleaseVariableCounter, m_variableCounter); } } void ContainerWizard::OnContainerTypeChanged(int index) { if (index >= 0 && index < m_genericContainerTypeNames.size()) { InitializeDisplay(m_genericContainerTypeNames[index].second); } } void ContainerWizard::OnTypeChanged(int index, const AZ::TypeId& typeId) { if (index >= 0 && index < m_containerTypes.size()) { m_containerTypes[index] = typeId; ReparseDisplay(); } } void ContainerWizard::OnDataTypeMenuVisibilityChanged(bool visible) { m_dataTypeMenuVisibile = visible; } void ContainerWizard::ValidateName(const QString& newName) { if (m_validationAction) { m_ui->variableName->removeAction(m_validationAction); delete m_validationAction; m_validationAction = nullptr; } ScriptCanvas::VariableValidationOutcome validName = AZ::Failure(ScriptCanvas::GraphVariableValidationErrorCode::Unknown); ScriptCanvas::GraphVariableManagerRequestBus::EventResult(validName, m_activeScriptCanvasId, &ScriptCanvas::GraphVariableManagerRequests::IsNameValid, newName.toUtf8().data()); if (!validName || newName.isEmpty()) { m_validationAction = m_ui->variableName->addAction(m_invalidIcon, QLineEdit::TrailingPosition); if (validName.GetError() == ScriptCanvas::GraphVariableValidationErrorCode::Invalid) { m_validationAction->setToolTip("A Variable name cannot be empty or over 200 characters.\nPlease specify a new name for the variable."); } else if (validName.GetError() == ScriptCanvas::GraphVariableValidationErrorCode::Duplicate) { m_validationAction->setToolTip("This name is already in use by\nanother variable"); } } m_ui->createButton->setEnabled((validName && !newName.isEmpty())); } void ContainerWizard::ClearDisplay() { m_containerTypes.clear(); while (m_ui->typeSelectionFrame->layout()->count() > 0) { QLayoutItem* layoutItem = m_ui->typeSelectionFrame->layout()->takeAt(0); } for (ContainerTypeLineEdit* lineEdit : m_containerTypeLineEdit) { if (lineEdit->isVisible()) { lineEdit->ResetLineEdit(); lineEdit->setVisible(false); } else { break; } } } void ContainerWizard::InitializeDisplay(const AZ::TypeId& typeId) { m_genericType = typeId; if (AZ::Utils::IsMapContainerType(m_genericType)) { PopulateMapDisplay(); } else { PopulateGeneralDisplay(); } adjustSize(); } void ContainerWizard::PopulateMapDisplay() { const AZStd::vector< AZStd::string > typeLabels = { "Key", "Value" }; PopulateGeneralDisplay("Map %i", "Map", typeLabels); } void ContainerWizard::PopulateGeneralDisplay(const AZStd::string& patternFallback, const AZStd::string& singleTypeString, const AZStd::vector< AZStd::string >& typeLabels) { ClearDisplay(); AZ::Crc32 workingCrc = AZ::Crc32(m_genericType.ToString().c_str()); int containerIndex = 0; auto dataTypeIter = m_containerDataTypeSets.find(workingCrc); QWidget* focusWidget = m_ui->variableName; while (dataTypeIter != m_containerDataTypeSets.end()) { DataTypeSet& dataTypeSet = dataTypeIter->second; ContainerTypeLineEdit* lineEdit = GetLineEdit(containerIndex); lineEdit->ResetLineEdit(); if (containerIndex >= typeLabels.size()) { AZStd::string containerName = AZStd::string::format(patternFallback.c_str(), containerIndex); lineEdit->SetDisplayName(containerName); } else { lineEdit->SetDisplayName(typeLabels[containerIndex]); } lineEdit->SetDataTypes(dataTypeSet); lineEdit->setVisible(true); m_ui->typeSelectionFrame->layout()->addWidget(lineEdit); AZ::TypeId typeId = lineEdit->GetDefaultTypeId(); workingCrc.Add(typeId.ToString().c_str()); m_containerTypes.emplace_back(typeId); { QSignalBlocker signalBlocker(lineEdit); lineEdit->DisplayType(typeId); } QWidget* nextFocus = lineEdit->GetLineEdit(); setTabOrder(focusWidget, nextFocus); focusWidget = nextFocus; ++containerIndex; dataTypeIter = m_containerDataTypeSets.find(workingCrc); } setTabOrder(focusWidget, m_ui->createButton); if (containerIndex == 1) { ContainerTypeLineEdit* lineEdit = GetLineEdit(0); lineEdit->SetDisplayName(singleTypeString); } } void ContainerWizard::RegisterDataType(const AZ::TypeId& dataType) { m_dataTypes[dataType] = ScriptCanvasEditor::TranslationHelper::GetSafeTypeName(ScriptCanvas::Data::FromAZType(dataType)); } void ContainerWizard::RegisterContainerType(const AZ::TypeId& containerType) { if (m_serializeContext == nullptr) { AZ::ComponentApplicationBus::BroadcastResult(m_serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext); if (m_serializeContext == nullptr) { AZ_Warning("ScriptCanvas", false, "Not given a SerializeContext and unable to find a SerializeContext to deduce generic ContainerTypes from."); } } AZStd::vector containedTypes = AZ::Utils::GetContainedTypes(containerType); AZ::GenericClassInfo* classInfo = m_serializeContext->FindGenericClassInfo(containerType); if (classInfo) { // Until we get the ability to create generic versions of these containers // Keep track of all of the partial matches so we can generate various lists in order to pretend // that we have general Key/Value support. // // Using the Partial CRC to identify a partial match so we can sort of soft fill out our template by // knowing all of the possible outcomes given Container X with first element Y AZ::Crc32 workingCrc = AZ::Crc32(classInfo->GetGenericTypeId().ToString().c_str()); for (const AZ::Uuid& containedType : containedTypes) { DataTypeSet& dataTypeSet = m_containerDataTypeSets[workingCrc]; if (ScriptCanvas::Data::IsNumber(containedType)) { dataTypeSet.insert(azrtti_typeid()); workingCrc.Add(containedType.ToString().c_str()); } else { dataTypeSet.insert(containedType); workingCrc.Add(containedType.ToString().c_str()); } } if (m_finalContainerTypeIds.find(workingCrc) == m_finalContainerTypeIds.end()) { m_finalContainerTypeIds[workingCrc] = containerType; } // Need to populate the combo box AZ::TypeId genericTypeId = AZ::Utils::GetGenericContainerType(containerType); auto insertResult = m_genericContainerTypes.insert(genericTypeId); if (insertResult.second) { AZ::BehaviorContext* behaviorContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(behaviorContext, &AZ::ComponentApplicationRequests::GetBehaviorContext); if (behaviorContext) { auto mapIter = behaviorContext->m_typeToClassMap.find(containerType); if (mapIter != behaviorContext->m_typeToClassMap.end()) { AZ::Attribute* categoryAttribute = AZ::FindAttribute(AZ::Script::Attributes::Category, mapIter->second->m_attributes); AZStd::string categoryName; if (categoryAttribute) { if (AZ::AttributeReader(nullptr, categoryAttribute).Read(categoryName, *behaviorContext)) { m_genericContainerTypeNames.emplace_back(categoryName, genericTypeId); } else { m_genericContainerTypes.erase(genericTypeId); } } AZ::Attribute* tooltipAttribute = AZ::FindAttribute(AZ::Script::Attributes::ToolTip, mapIter->second->m_attributes); if (tooltipAttribute) { AZStd::string containerToolTip; if (AZ::AttributeReader(nullptr, tooltipAttribute).Read(containerToolTip, *behaviorContext)) { QString toolTip = m_ui->containerLabel->toolTip(); // We want to sort the container tool tips so the order they are added matches the order in which // we display them in the combobox. // // I don't really have a final point at which to resolve this currently, so I'm just going to sort the elements on the // fly with a ton of string manipulation to get it to align correctly. QStringList previousToolTips = toolTip.split("\n"); if (!previousToolTips.empty()) { toolTip.clear(); // First tooltip is always the native tool tip. So we want to maintain that toolTip.append(previousToolTips[0]); previousToolTips.removeFirst(); QString newToolTip = QString(" %1 - %2").arg(categoryName.c_str()).arg(containerToolTip.c_str()); previousToolTips.push_back(newToolTip); previousToolTips.sort(Qt::CaseInsensitive); for (const QString& toolTipString : previousToolTips) { toolTip.append("\n"); toolTip.append(toolTipString); } m_ui->containerLabel->setToolTip(toolTip); } } } } } } } else { AZ_Warning("ScriptCanvas", false, "Could not find generic class info for container with TypeId(%s)", containerType.ToString().c_str()); } } ContainerTypeLineEdit* ContainerWizard::GetLineEdit(int typeIndex) { while (m_containerTypeLineEdit.size() <= typeIndex) { ContainerTypeLineEdit* lineEdit = aznew ContainerTypeLineEdit(typeIndex, this); QObject::connect(lineEdit, &ContainerTypeLineEdit::TypeChanged, this, &ContainerWizard::OnTypeChanged); QObject::connect(lineEdit, &ContainerTypeLineEdit::DataTypeMenuVisibilityChanged, this, &ContainerWizard::OnDataTypeMenuVisibilityChanged); m_containerTypeLineEdit.emplace_back(lineEdit); } return m_containerTypeLineEdit[typeIndex]; } } #include