#include "CPropertyDelegate.h" #include "CPropertyRelay.h" #include "Editor/UICommon.h" #include "Editor/Undo/CEditScriptPropertyCommand.h" #include "Editor/Undo/CEditIntrinsicPropertyCommand.h" #include "Editor/Undo/CResizeScriptArrayCommand.h" #include "Editor/Widgets/CResourceSelector.h" #include "Editor/Widgets/WColorPicker.h" #include "Editor/Widgets/WDraggableSpinBox.h" #include "Editor/Widgets/WIntegralSpinBox.h" #include #include #include #include #include #include // This macro should be used on every widget where changes should be reflected in realtime and not just when the edit is finished. #define CONNECT_RELAY(Widget, Index, Signal) \ { \ CPropertyRelay *pRelay = new CPropertyRelay(Widget, Index); \ connect(Widget, SIGNAL(Signal), pRelay, SLOT(OnWidgetEdited())); \ connect(pRelay, SIGNAL(WidgetEdited(QWidget*, const QModelIndex&)), this, SLOT(WidgetEdited(QWidget*, const QModelIndex&))); \ } CPropertyDelegate::CPropertyDelegate(QObject* pParent /*= 0*/) : QStyledItemDelegate(pParent) , mpEditor(nullptr) , mpModel(nullptr) , mInRelayWidgetEdit(false) , mEditInProgress(false) , mRelaysBlocked(false) { mpEditor = UICommon::FindAncestor(pParent); } void CPropertyDelegate::SetEditor(IEditor* pEditor) { mpEditor = pEditor; } void CPropertyDelegate::SetPropertyModel(CPropertyModel* pModel) { mpModel = pModel; } QWidget* CPropertyDelegate::createEditor(QWidget* pParent, const QStyleOptionViewItem& /*rkOption*/, const QModelIndex& rkIndex) const { if (!mpModel) return nullptr; IProperty *pProp = mpModel->PropertyForIndex(rkIndex, false); QWidget *pOut = nullptr; if (pProp) { EPropertyType Type = mpModel->GetEffectiveFieldType(pProp); switch (Type) { case EPropertyType::Bool: { QCheckBox *pCheckBox = new QCheckBox(pParent); CONNECT_RELAY(pCheckBox, rkIndex, toggled(bool)) pOut = pCheckBox; break; } case EPropertyType::Short: { WIntegralSpinBox *pSpinBox = new WIntegralSpinBox(pParent); pSpinBox->setMinimum(INT16_MIN); pSpinBox->setMaximum(INT16_MAX); pSpinBox->setSuffix(TO_QSTRING(pProp->Suffix())); CONNECT_RELAY(pSpinBox, rkIndex, valueChanged(int)) pOut = pSpinBox; break; } case EPropertyType::Int: { WIntegralSpinBox *pSpinBox = new WIntegralSpinBox(pParent); pSpinBox->setMinimum(INT32_MIN); pSpinBox->setMaximum(INT32_MAX); pSpinBox->setSuffix(TO_QSTRING(pProp->Suffix())); CONNECT_RELAY(pSpinBox, rkIndex, valueChanged(int)) pOut = pSpinBox; break; } case EPropertyType::Float: { WDraggableSpinBox *pSpinBox = new WDraggableSpinBox(pParent); pSpinBox->setSingleStep(0.1); pSpinBox->setSuffix(TO_QSTRING(pProp->Suffix())); CONNECT_RELAY(pSpinBox, rkIndex, valueChanged(double)) pOut = pSpinBox; break; } case EPropertyType::Color: { WColorPicker *pColorPicker = new WColorPicker(pParent); CONNECT_RELAY(pColorPicker, rkIndex, ColorChanged(QColor)) pOut = pColorPicker; break; } case EPropertyType::Sound: { WIntegralSpinBox *pSpinBox = new WIntegralSpinBox(pParent); pSpinBox->setMinimum(-1); pSpinBox->setMaximum(0xFFFF); CONNECT_RELAY(pSpinBox, rkIndex, valueChanged(int)) pOut = pSpinBox; break; } case EPropertyType::String: { QLineEdit *pLineEdit = new QLineEdit(pParent); CONNECT_RELAY(pLineEdit, rkIndex, textEdited(QString)) pOut = pLineEdit; break; } case EPropertyType::Enum: case EPropertyType::Choice: { QComboBox *pComboBox = new QComboBox(pParent); CEnumProperty* pEnum = TPropCast(pProp); for (uint32 ValueIdx = 0; ValueIdx < pEnum->NumPossibleValues(); ValueIdx++) pComboBox->addItem(TO_QSTRING(pEnum->ValueName(ValueIdx))); CONNECT_RELAY(pComboBox, rkIndex, currentIndexChanged(int)) pOut = pComboBox; break; } case EPropertyType::Asset: { CResourceSelector *pSelector = new CResourceSelector(pParent); pSelector->SetFrameVisible(false); CAssetProperty *pAsset = TPropCast(pProp); pSelector->SetTypeFilter(pAsset->GetTypeFilter()); CONNECT_RELAY(pSelector, rkIndex, ResourceChanged(CResourceEntry*)) pOut = pSelector; break; } case EPropertyType::Array: { // No relay here, would prefer user to be sure of their change before it's reflected on the UI WIntegralSpinBox *pSpinBox = new WIntegralSpinBox(pParent); pSpinBox->setMinimum(0); pSpinBox->setMaximum(999); pOut = pSpinBox; break; } default: break; } } // Check for sub-property of flags/animation set else if (rkIndex.internalId() & 0x80000000) { pProp = mpModel->PropertyForIndex(rkIndex, true); EPropertyType Type = mpModel->GetEffectiveFieldType(pProp); // Handle character if (Type == EPropertyType::AnimationSet) pOut = CreateCharacterEditor(pParent, rkIndex); // Handle flags else if (Type == EPropertyType::Flags) { QCheckBox *pCheckBox = new QCheckBox(pParent); CONNECT_RELAY(pCheckBox, rkIndex, toggled(bool)) pOut = pCheckBox; } } if (pOut) { pOut->setFocusPolicy(Qt::StrongFocus); QSize Size = mpModel->data(rkIndex, Qt::SizeHintRole).toSize(); pOut->setFixedHeight(Size.height()); } return pOut; } void CPropertyDelegate::setEditorData(QWidget *pEditor, const QModelIndex &rkIndex) const { BlockRelays(true); mEditInProgress = false; // fixes case where user does undo mid-edit if (pEditor) { // Set editor data for regular property IProperty *pProp = mpModel->PropertyForIndex(rkIndex, false); void* pData = mpModel->DataPointerForIndex(rkIndex); if (pProp) { if (!mEditInProgress) { EPropertyType Type = mpModel->GetEffectiveFieldType(pProp); switch (Type) { case EPropertyType::Bool: { QCheckBox *pCheckBox = static_cast(pEditor); CBoolProperty *pBool = TPropCast(pProp); pCheckBox->setChecked( pBool->Value(pData) ); break; } case EPropertyType::Short: { WIntegralSpinBox *pSpinBox = static_cast(pEditor); if (!pSpinBox->hasFocus()) { CShortProperty *pShort = TPropCast(pProp); pSpinBox->setValue( pShort->Value(pData) ); } break; } case EPropertyType::Int: { WIntegralSpinBox *pSpinBox = static_cast(pEditor); if (!pSpinBox->hasFocus()) { // Ints use static_cast since sometimes we treat other property types as ints CIntProperty *pInt = static_cast(pProp); pSpinBox->setValue( pInt->Value(pData) ); } break; } case EPropertyType::Sound: { WIntegralSpinBox *pSpinBox = static_cast(pEditor); if (!pSpinBox->hasFocus()) { CSoundProperty *pSound = TPropCast(pProp); pSpinBox->setValue( pSound->Value(pData) ); } break; } case EPropertyType::Float: { WDraggableSpinBox *pSpinBox = static_cast(pEditor); if (!pSpinBox->hasFocus()) { CFloatProperty *pFloat = TPropCast(pProp); pSpinBox->setValue( pFloat->Value(pData) ); } break; } case EPropertyType::Color: { WColorPicker *pColorPicker = static_cast(pEditor); CColorProperty *pColor = TPropCast(pProp); CColor Color = pColor->Value(pData); pColorPicker->SetColor(TO_QCOLOR(Color)); break; } case EPropertyType::String: { QLineEdit *pLineEdit = static_cast(pEditor); if (!pLineEdit->hasFocus()) { CStringProperty *pString = TPropCast(pProp); pLineEdit->setText( TO_QSTRING(pString->Value(pData)) ); } break; } case EPropertyType::Enum: case EPropertyType::Choice: { QComboBox *pComboBox = static_cast(pEditor); CEnumProperty* pEnum = TPropCast(pProp); pComboBox->setCurrentIndex( pEnum->ValueIndex( pEnum->Value(pData) ) ); break; } case EPropertyType::Asset: { CResourceSelector *pSelector = static_cast(pEditor); CAssetProperty *pAsset = TPropCast(pProp); pSelector->SetResource(pAsset->Value(pData)); break; } case EPropertyType::Array: { WIntegralSpinBox *pSpinBox = static_cast(pEditor); if (!pSpinBox->hasFocus()) { CArrayProperty *pArray = static_cast(pProp); pSpinBox->setValue( pArray->ArrayCount(pData) ); } break; } default: break; } } } // Set editor data for animation set/flags sub-property else if (rkIndex.internalId() & 0x80000000) { pProp = mpModel->PropertyForIndex(rkIndex, true); EPropertyType Type = mpModel->GetEffectiveFieldType(pProp); if (Type == EPropertyType::AnimationSet) SetCharacterEditorData(pEditor, rkIndex); else if (Type == EPropertyType::Flags) { QCheckBox *pCheckBox = static_cast(pEditor); CFlagsProperty* pFlags = TPropCast(pProp); uint32 Mask = pFlags->FlagMask(rkIndex.row()); bool Set = (pFlags->Value(pData) & Mask) != 0; pCheckBox->setChecked(Set); } } } BlockRelays(false); } void CPropertyDelegate::setModelData(QWidget *pEditor, QAbstractItemModel* /*pModel*/, const QModelIndex &rkIndex) const { if (!mpModel) return; if (!pEditor) return; IEditPropertyCommand* pCommand = nullptr; IProperty *pProp = mpModel->PropertyForIndex(rkIndex, true); void* pData = mpModel->DataPointerForIndex(rkIndex); if (pProp) { EPropertyType Type = mpModel->GetEffectiveFieldType(pProp); CScriptObject* pObject = mpModel->GetScriptObject(); if (!pObject) { QVector DataPointers; DataPointers << pData; pCommand = new CEditIntrinsicPropertyCommand(pProp, DataPointers, mpModel, rkIndex); } else { QVector Objects; Objects << pObject; pCommand = (Type != EPropertyType::Array) ? new CEditScriptPropertyCommand(pProp, Objects, mpModel, rkIndex) : new CResizeScriptArrayCommand (pProp, Objects, mpModel, rkIndex); } pCommand->SaveOldData(); if (Type != EPropertyType::Array) { // Handle sub-properties of flags and animation sets if (rkIndex.internalId() & 0x80000000) { if (Type == EPropertyType::AnimationSet) SetCharacterModelData(pEditor, rkIndex); else if (Type == EPropertyType::Flags) { QCheckBox* pCheckBox = static_cast(pEditor); CFlagsProperty* pFlags = static_cast(pProp); uint32 Mask = pFlags->FlagMask(rkIndex.row()); int Flags = pFlags->Value(pData); if (pCheckBox->isChecked()) Flags |= Mask; else Flags &= ~Mask; pFlags->ValueRef(pData) = Flags; } } else { switch (Type) { case EPropertyType::Bool: { QCheckBox *pCheckBox = static_cast(pEditor); CBoolProperty* pBool = static_cast(pProp); pBool->ValueRef(pData) = pCheckBox->isChecked(); break; } case EPropertyType::Short: { WIntegralSpinBox* pSpinBox = static_cast(pEditor); CShortProperty* pShort = static_cast(pProp); pShort->ValueRef(pData) = pSpinBox->value(); break; } case EPropertyType::Int: { // Ints use static_cast since sometimes we treat other property types as ints WIntegralSpinBox* pSpinBox = static_cast(pEditor); CIntProperty* pInt = static_cast(pProp); pInt->ValueRef(pData) = pSpinBox->value(); break; } case EPropertyType::Sound: { WIntegralSpinBox* pSpinBox = static_cast(pEditor); CSoundProperty* pSound = static_cast(pProp); pSound->ValueRef(pData) = pSpinBox->value(); break; } case EPropertyType::Float: { WDraggableSpinBox* pSpinBox = static_cast(pEditor); CFloatProperty* pFloat = static_cast(pProp); pFloat->ValueRef(pData) = (float) pSpinBox->value(); break; } case EPropertyType::Color: { WColorPicker* pColorPicker = static_cast(pEditor); CColorProperty* pColor = static_cast(pProp); QColor Color = pColorPicker->Color(); pColor->ValueRef(pData) = TO_CCOLOR(Color); break; } case EPropertyType::String: { QLineEdit* pLineEdit = static_cast(pEditor); CStringProperty* pString = static_cast(pProp); pString->ValueRef(pData) = TO_TSTRING(pLineEdit->text()); break; } case EPropertyType::Enum: case EPropertyType::Choice: { QComboBox* pComboBox = static_cast(pEditor); CEnumProperty* pEnum = static_cast(pProp); pEnum->ValueRef(pData) = pEnum->ValueID(pComboBox->currentIndex()); break; } case EPropertyType::Asset: { CResourceSelector* pSelector = static_cast(pEditor); CResourceEntry* pEntry = pSelector->Entry(); CAssetProperty* pAsset = static_cast(pProp); pAsset->ValueRef(pData) = (pEntry ? pEntry->ID() : CAssetID::InvalidID(pAsset->Game())); break; } default: break; } } pCommand->SaveNewData(); } // Array else { WIntegralSpinBox* pSpinBox = static_cast(pEditor); CArrayProperty* pArray = static_cast(pProp); int OldCount = pArray->ArrayCount(pData); int NewCount = pSpinBox->value(); if (OldCount != NewCount) { mpModel->ArrayAboutToBeResized(rkIndex, NewCount); pArray->Resize(pData, NewCount); mpModel->ArrayResized(rkIndex, OldCount); } pCommand->SaveNewData(); } } if (pCommand) { // Check for edit in progress bool DataChanged = pCommand->IsNewDataDifferent(); if (DataChanged && mInRelayWidgetEdit && (pEditor->hasFocus() || pProp->Type() == EPropertyType::Color)) mEditInProgress = true; bool EditWasInProgress = mEditInProgress; // Check for edit finished if (!mInRelayWidgetEdit || (!pEditor->hasFocus() && pProp->Type() != EPropertyType::Color)) mEditInProgress = false; // Push undo command if (DataChanged || EditWasInProgress) { // Always consider the edit done for bool properties pCommand->SetEditComplete(!mEditInProgress || pProp->Type() == EPropertyType::Bool); mpEditor->UndoStack().push(pCommand); } else delete pCommand; } } bool CPropertyDelegate::eventFilter(QObject *pObject, QEvent *pEvent) { if (pEvent->type() == QEvent::Wheel) { QWidget *pWidget = static_cast(pObject); if (!pWidget->hasFocus()) return true; pEvent->ignore(); return false; } return QStyledItemDelegate::eventFilter(pObject, pEvent); } // Character properties have separate functions because they're somewhat complicated - they have different layouts in different games QWidget* CPropertyDelegate::CreateCharacterEditor(QWidget *pParent, const QModelIndex& rkIndex) const { CAnimationSetProperty* pAnimSetProp = TPropCast(mpModel->PropertyForIndex(rkIndex, true)); CAnimationParameters Params = pAnimSetProp->Value(mpModel->DataPointerForIndex(rkIndex)); // Determine property type EPropertyType Type = DetermineCharacterPropType(Params.Version(), rkIndex); // Create widget if (Type == EPropertyType::Asset) { CResourceSelector* pSelector = new CResourceSelector(pParent); pSelector->SetFrameVisible(false); if (Params.Version() <= EGame::Echoes) pSelector->SetTypeFilter(gpEdApp->CurrentGame(), "ANCS"); else pSelector->SetTypeFilter(gpEdApp->CurrentGame(), "CHAR"); CONNECT_RELAY(pSelector, rkIndex, ResourceChanged(CResourceEntry*)); return pSelector; } else if (Type == EPropertyType::Enum || Type == EPropertyType::Choice) { QComboBox* pComboBox = new QComboBox(pParent); CAnimSet* pAnimSet = Params.AnimSet(); if (pAnimSet) { for (size_t CharIdx = 0; CharIdx < pAnimSet->NumCharacters(); CharIdx++) pComboBox->addItem(TO_QSTRING(pAnimSet->Character(CharIdx)->Name)); } CONNECT_RELAY(pComboBox, rkIndex, currentIndexChanged(int)); return pComboBox; } else if (Type == EPropertyType::Int) { WIntegralSpinBox *pSpinBox = new WIntegralSpinBox(pParent); CONNECT_RELAY(pSpinBox, rkIndex, valueChanged(int)); return pSpinBox; } return nullptr; } void CPropertyDelegate::SetCharacterEditorData(QWidget *pEditor, const QModelIndex& rkIndex) const { CAnimationSetProperty* pAnimSetProp = TPropCast(mpModel->PropertyForIndex(rkIndex, true)); CAnimationParameters Params = pAnimSetProp->Value(mpModel->DataPointerForIndex(rkIndex)); EPropertyType Type = DetermineCharacterPropType(Params.Version(), rkIndex); if (Type == EPropertyType::Asset) { static_cast(pEditor)->SetResource(Params.AnimSet()); } else if (Type == EPropertyType::Enum || Type == EPropertyType::Choice) { static_cast(pEditor)->setCurrentIndex(Params.CharacterIndex()); } else if (Type == EPropertyType::Int && !pEditor->hasFocus()) { int UnkIndex = (Params.Version() <= EGame::Echoes ? rkIndex.row() - 2 : rkIndex.row() - 1); uint32 Value = Params.Unknown(UnkIndex); static_cast(pEditor)->setValue(Value); } } void CPropertyDelegate::SetCharacterModelData(QWidget *pEditor, const QModelIndex& rkIndex) const { CAnimationSetProperty* pAnimSetProp = TPropCast(mpModel->PropertyForIndex(rkIndex, true)); CAnimationParameters Params = pAnimSetProp->Value(mpModel->DataPointerForIndex(rkIndex)); EPropertyType Type = DetermineCharacterPropType(Params.Version(), rkIndex); if (Type == EPropertyType::Asset) { CResourceEntry *pEntry = static_cast(pEditor)->Entry(); Params.SetResource( pEntry ? pEntry->ID() : CAssetID::InvalidID(gpEdApp->CurrentGame()) ); } else if (Type == EPropertyType::Enum || Type == EPropertyType::Choice) { Params.SetCharIndex( static_cast(pEditor)->currentIndex() ); } else if (Type == EPropertyType::Int) { int UnkIndex = (Params.Version() <= EGame::Echoes ? rkIndex.row() - 2 : rkIndex.row() - 1); Params.SetUnknown(UnkIndex, static_cast(pEditor)->value() ); } pAnimSetProp->ValueRef(mpModel->DataPointerForIndex(rkIndex)) = Params; // If we just updated the resource, make sure all the sub-properties of the character are flagged as changed. // We want to do this -after- updating the anim params on the property, which is why we have a second type check. if (Type == EPropertyType::Asset) { QModelIndex ParentIndex = rkIndex.parent(); mpModel->dataChanged(mpModel->index(1, 1, ParentIndex), mpModel->index(mpModel->rowCount(ParentIndex) - 1, 1, ParentIndex)); } } EPropertyType CPropertyDelegate::DetermineCharacterPropType(EGame Game, const QModelIndex& rkIndex) const { if (Game <= EGame::Echoes) { if (rkIndex.row() == 0) return EPropertyType::Asset; else if (rkIndex.row() == 1) return EPropertyType::Choice; else if (rkIndex.row() == 2) return EPropertyType::Int; } else if (Game <= EGame::Corruption) { if (rkIndex.row() == 0) return EPropertyType::Asset; else if (rkIndex.row() == 1) return EPropertyType::Int; } else { if (rkIndex.row() == 0) return EPropertyType::Asset; else if (rkIndex.row() <= 2) return EPropertyType::Int; } return EPropertyType::Invalid; } // ************ PUBLIC SLOTS ************ void CPropertyDelegate::WidgetEdited(QWidget *pWidget, const QModelIndex& rkIndex) { // This slot is used to update property values as they're being updated so changes can be // reflected in realtime in other parts of the application. mInRelayWidgetEdit = true; if (!mRelaysBlocked) setModelData(pWidget, mpModel, rkIndex); mInRelayWidgetEdit = false; }