Repository: RetroAchievements/RAIntegration
Pull request #657 (2021-01-02 20:43:38)

hide target fields for modifying operators
Hides the operator, target type, target size, target value, and hit count fields when selecting AddAddress, AddSource, or SubSource condition flags.


Patch:
diff --git a/src/ui/ViewModelCollection.hh b/src/ui/ViewModelCollection.hh
index f3326930..e4282e76 100644
--- a/src/ui/ViewModelCollection.hh
+++ b/src/ui/ViewModelCollection.hh
@@ -74,6 +74,16 @@ public:
         }
     }
 
+    /// <summary>
+    /// Gets the <see cref="ViewModelBase" /> at the specified index.
+    /// </summary>
+    ViewModelBase* GetViewModelAt(gsl::index nIndex) { return dynamic_cast<ViewModelBase*>(GetModelAt(nIndex)); }
+
+    /// <summary>
+    /// Gets the <see cref="ViewModelBase" /> at the specified index.
+    /// </summary>
+    const ViewModelBase* GetViewModelAt(gsl::index nIndex) const { return dynamic_cast<const ViewModelBase*>(GetModelAt(nIndex)); }
+
 protected:
     void OnModelValueChanged(gsl::index nIndex, const BoolModelProperty::ChangeArgs& args) override;
     void OnModelValueChanged(gsl::index nIndex, const StringModelProperty::ChangeArgs& args) override;
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 125cd1e9..8af63498 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -24,6 +24,10 @@ const IntModelProperty TriggerConditionViewModel::TargetValueProperty("TriggerCo
 const IntModelProperty TriggerConditionViewModel::CurrentHitsProperty("TriggerConditionViewModel", "CurrentHits", 0);
 const IntModelProperty TriggerConditionViewModel::RequiredHitsProperty("TriggerConditionViewModel", "RequiredHits", 0);
 const BoolModelProperty TriggerConditionViewModel::IsSelectedProperty("TriggerConditionViewModel", "IsSelected", false);
+const BoolModelProperty TriggerConditionViewModel::HasSourceSizeProperty("TriggerConditionViewModel", "HasSourceSize", true);
+const BoolModelProperty TriggerConditionViewModel::HasTargetProperty("TriggerConditionViewModel", "HasTarget", true);
+const BoolModelProperty TriggerConditionViewModel::HasTargetSizeProperty("TriggerConditionViewModel", "HasTargetSize", false);
+const BoolModelProperty TriggerConditionViewModel::HasHitsProperty("TriggerConditionViewModel", "HasHits", true);
 
 std::string TriggerConditionViewModel::Serialize() const
 {
@@ -240,6 +244,51 @@ void TriggerConditionViewModel::InitializeFrom(const rc_condition_t& pCondition)
     SetRequiredHits(pCondition.required_hits);
 }
 
+void TriggerConditionViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
+{
+    if (args.Property == SourceTypeProperty)
+    {
+        SetValue(HasSourceSizeProperty, ra::itoe<TriggerOperandType>(args.tNewValue) != TriggerOperandType::Value);
+    }
+    else if (args.Property == TargetTypeProperty)
+    {
+        SetValue(HasTargetSizeProperty, ra::itoe<TriggerOperandType>(args.tNewValue) != TriggerOperandType::Value && GetValue(HasTargetProperty));
+    }
+    else if (args.Property == OperatorProperty)
+    {
+        SetValue(HasTargetProperty, ra::itoe<TriggerOperatorType>(args.tNewValue) != TriggerOperatorType::None);
+    }
+    else if (args.Property == TypeProperty)
+    {
+        const auto nOldType = ra::itoe<TriggerConditionType>(args.tOldValue);
+        const auto nNewType = ra::itoe<TriggerConditionType>(args.tNewValue);
+        const auto bIsModifying = IsModifying(nNewType);
+        if (bIsModifying != IsModifying(nOldType))
+        {
+            if (bIsModifying)
+            {
+                SetValue(HasHitsProperty, false);
+                SetOperator(TriggerOperatorType::None);
+            }
+            else
+            {
+                SetValue(HasHitsProperty, true);
+                SetOperator(TriggerOperatorType::Equals);
+            }
+        }
+    }
+
+    ViewModelBase::OnValueChanged(args);
+}
+
+void TriggerConditionViewModel::OnValueChanged(const BoolModelProperty::ChangeArgs& args)
+{
+    if (args.Property == HasTargetProperty)
+        SetValue(HasTargetSizeProperty, args.tNewValue && GetTargetType() != TriggerOperandType::Value);
+
+    ViewModelBase::OnValueChanged(args);
+}
+
 std::wstring TriggerConditionViewModel::GetTooltip(const IntModelProperty& nProperty) const
 {
     if (nProperty == SourceValueProperty)
@@ -263,7 +312,7 @@ std::wstring TriggerConditionViewModel::GetValueTooltip(TriggerOperandType nType
     return std::to_wstring(nValue);
 }
 
-std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned nAddress) const
+std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned int nAddress) const
 {
     const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
     const auto* pNote = pGameContext.FindCodeNote(nAddress);
@@ -292,6 +341,40 @@ std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned nAddress) con
     return ra::StringPrintf(L"%s\r\n%s", ra::ByteAddressToString(nAddress), *pNote);
 }
 
+bool TriggerConditionViewModel::IsModifying(TriggerConditionType nType) noexcept
+{
+    switch (nType)
+    {
+        case TriggerConditionType::AddAddress:
+        case TriggerConditionType::AddSource:
+        case TriggerConditionType::SubSource:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+bool TriggerConditionViewModel::IsComparisonVisible(const ViewModelBase& vmItem, int nValue)
+{
+    const auto* vmCondition = dynamic_cast<const TriggerConditionViewModel*>(&vmItem);
+    if (vmCondition == nullptr)
+        return false;
+
+    const auto nComparison = ra::itoe<TriggerOperatorType>(nValue);
+    switch (nComparison)
+    {
+        case TriggerOperatorType::None:
+        case TriggerOperatorType::Multiply:
+        case TriggerOperatorType::Divide:
+        case TriggerOperatorType::BitwiseAnd:
+            return vmCondition->IsModifying();
+
+        default:
+            return !vmCondition->IsModifying();
+    }
+}
+
 } // namespace viewmodels
 } // namespace ui
 } // namespace ra
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index baf3fe85..1dbebcea 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -68,6 +68,7 @@ public:
     TriggerOperandType GetSourceType() const { return ra::itoe<TriggerOperandType>(GetValue(SourceTypeProperty)); }
     void SetSourceType(TriggerOperandType nValue) { SetValue(SourceTypeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasSourceSizeProperty;
     static const IntModelProperty SourceSizeProperty;
     MemSize GetSourceSize() const { return ra::itoe<MemSize>(GetValue(SourceSizeProperty)); }
     void SetSourceSize(MemSize nValue) { SetValue(SourceSizeProperty, ra::etoi(nValue)); }
@@ -80,10 +81,12 @@ public:
     TriggerOperatorType GetOperator() const { return ra::itoe<TriggerOperatorType>(GetValue(OperatorProperty)); }
     void SetOperator(TriggerOperatorType nValue) { SetValue(OperatorProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasTargetProperty;
     static const IntModelProperty TargetTypeProperty;
     TriggerOperandType GetTargetType() const { return ra::itoe<TriggerOperandType>(GetValue(TargetTypeProperty)); }
     void SetTargetType(TriggerOperandType nValue) { SetValue(TargetTypeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasTargetSizeProperty;
     static const IntModelProperty TargetSizeProperty;
     MemSize GetTargetSize() const { return ra::itoe<MemSize>(GetValue(TargetSizeProperty)); }
     void SetTargetSize(MemSize nValue) { SetValue(TargetSizeProperty, ra::etoi(nValue)); }
@@ -92,6 +95,7 @@ public:
     unsigned int GetTargetValue() const { return ra::to_unsigned(GetValue(TargetValueProperty)); }
     void SetTargetValue(unsigned int nValue) { SetValue(TargetValueProperty, ra::to_signed(nValue)); }
 
+    static const BoolModelProperty HasHitsProperty;
     static const IntModelProperty CurrentHitsProperty;
     unsigned int GetCurrentHits() const { return ra::to_unsigned(GetValue(CurrentHitsProperty)); }
     void SetCurrentHits(unsigned int nValue) { SetValue(CurrentHitsProperty, ra::to_signed(nValue)); }
@@ -111,14 +115,23 @@ public:
 
     std::wstring GetTooltip(const IntModelProperty& nProperty) const;
 
+    bool IsModifying() const { return IsModifying(GetType()); }
+
+    static bool IsComparisonVisible(const ViewModelBase& vmItem, int nValue);
+
 private:
+    void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
+    void OnValueChanged(const BoolModelProperty::ChangeArgs& args) override;
+
     void SerializeAppendOperand(std::string& sBuffer, TriggerOperandType nType, MemSize nSize, unsigned int nValue) const;
 
-    std::wstring GetValueTooltip(TriggerOperandType nType, unsigned nAddress) const;
-    std::wstring GetAddressTooltip(unsigned nAddress) const;
+    std::wstring GetValueTooltip(TriggerOperandType nType, unsigned int nValue) const;
+    std::wstring GetAddressTooltip(unsigned int nAddress) const;
 
     void SetOperand(const IntModelProperty& pTypeProperty, const IntModelProperty& pSizeProperty,
         const IntModelProperty& pValueProperty, const rc_operand_t& operand);
+
+    static bool IsModifying(TriggerConditionType nType) noexcept;
 };
 
 } // namespace viewmodels
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 627c74d2..14d293fb 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -64,28 +64,37 @@ void AssetEditorDialog::Presenter::OnClosed() noexcept { m_pDialog.reset(); }
 
 // ------------------------------------
 
-class SizeColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding
+class HideableGridLookupColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding
 {
 public:
-    SizeColumnBinding(const IntModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty, const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept
-        : ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems), m_pTypeProperty(&pTypeProperty)
+    HideableGridLookupColumnBinding(const IntModelProperty& pBoundProperty, const BoolModelProperty& pIsHiddenProperty,
+        const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept
+        : ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems), m_pIsHiddenProperty(pIsHiddenProperty)
     {
     }
 
     std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        if (IsValueHidden(vmItems, nIndex))
+        if (IsHidden(vmItems, nIndex))
             return L"";
 
         return GridLookupColumnBinding::GetText(vmItems, nIndex);
     }
 
-    bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override
+    std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        if (pProperty == *m_pTypeProperty)
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return GridLookupColumnBinding::GetTooltip(vmItems, nIndex);
+    }
+
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
+    {
+        if (pProperty == m_pIsHiddenProperty)
             return true;
 
-        return GridLookupColumnBinding::DependsOn(pProperty);
+        return GridColumnBinding::DependsOn(pProperty);
     }
 
     HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
@@ -93,20 +102,19 @@ public:
         auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
         Expects(pGridBinding != nullptr);
 
-        if (IsValueHidden(pGridBinding->GetItems(), pInfo.nItemIndex))
+        if (IsHidden(pGridBinding->GetItems(), pInfo.nItemIndex))
             return nullptr;
 
         return GridLookupColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
 
 protected:
-    bool IsValueHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
     {
-        const auto nOperandType = ra::itoe<ra::ui::viewmodels::TriggerOperandType>(vmItems.GetItemValue(nIndex, *m_pTypeProperty));
-        return (nOperandType == ra::ui::viewmodels::TriggerOperandType::Value);
+        return !vmItems.GetItemValue(nIndex, m_pIsHiddenProperty);
     }
 
-    const IntModelProperty* m_pTypeProperty = nullptr;
+    const BoolModelProperty& m_pIsHiddenProperty;
 };
 
 class ValueColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
@@ -198,6 +206,53 @@ protected:
     const IntModelProperty* m_pTypeProperty = nullptr;
 };
 
+class TargetValueColumnBinding : public ValueColumnBinding
+{
+public:
+    TargetValueColumnBinding(const IntModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
+        : ValueColumnBinding(pBoundProperty, pTypeProperty)
+    {
+    }
+
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return ValueColumnBinding::GetText(vmItems, nIndex);
+    }
+
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
+    {
+        if (pProperty == TriggerConditionViewModel::HasTargetProperty)
+            return true;
+
+        return GridColumnBinding::DependsOn(pProperty);
+    }
+
+    std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return ValueColumnBinding::GetTooltip(vmItems, nIndex);
+    }
+
+    bool HandleRightClick(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return false;
+
+        return ValueColumnBinding::HandleRightClick(vmItems, nIndex);
+    }
+
+protected:
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
+    {
+        return !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasTargetProperty);
+    }
+};
+
 class HitsColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
 {
 public:
@@ -208,6 +263,9 @@ public:
 
     std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
+        if (!vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasHitsProperty))
+            return L"";
+
         const auto nCurrentHits = vmItems.GetItemValue(nIndex, TriggerConditionViewModel::CurrentHitsProperty);
         const auto nRequiredHits = vmItems.GetItemValue(nIndex, *m_pBoundProperty);
 
@@ -224,12 +282,19 @@ public:
     {
         if (pProperty == TriggerConditionViewModel::CurrentHitsProperty)
             return true;
+        if (pProperty == TriggerConditionViewModel::HasHitsProperty)
+            return true;
 
         return GridNumberColumnBinding::DependsOn(pProperty);
     }
 
     HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
     {
+        auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
+        Expects(pGridBinding != nullptr);
+        if (!pGridBinding->GetItems().GetItemValue(pInfo.nItemIndex, TriggerConditionViewModel::HasHitsProperty))
+            return nullptr;
+
         m_nEditingItemIndex = pInfo.nItemIndex;
         return GridNumberColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
@@ -416,8 +481,8 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
     pSourceTypeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(2, std::move(pSourceTypeColumn));
 
-    auto pSourceSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::SourceSizeProperty,
-        TriggerConditionViewModel::SourceTypeProperty, vmAssetEditor.Trigger().OperandSizes());
+    auto pSourceSizeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::SourceSizeProperty,
+        TriggerConditionViewModel::HasSourceSizeProperty, vmAssetEditor.Trigger().OperandSizes());
     pSourceSizeColumn->SetHeader(L"Size");
     pSourceSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
     pSourceSizeColumn->SetReadOnly(false);
@@ -434,24 +499,25 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
         TriggerConditionViewModel::OperatorProperty, vmAssetEditor.Trigger().OperatorTypes());
     pOperatorColumn->SetHeader(L"Cmp");
     pOperatorColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_OPERATOR);
+    pOperatorColumn->SetVisibilityFilter(TriggerConditionViewModel::IsComparisonVisible);
     pOperatorColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(5, std::move(pOperatorColumn));
 
-    auto pTargetTypeColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
-        TriggerConditionViewModel::TargetTypeProperty, vmAssetEditor.Trigger().OperandTypes());
+    auto pTargetTypeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::TargetTypeProperty,
+        TriggerConditionViewModel::HasTargetProperty, vmAssetEditor.Trigger().OperandTypes());
     pTargetTypeColumn->SetHeader(L"Type");
     pTargetTypeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_TYPE);
     pTargetTypeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(6, std::move(pTargetTypeColumn));
 
-    auto pTargetSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::TargetSizeProperty,
-        TriggerConditionViewModel::TargetTypeProperty, vmAssetEditor.Trigger().OperandSizes());
+    auto pTargetSizeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::TargetSizeProperty,
+        TriggerConditionViewModel::HasTargetSizeProperty, vmAssetEditor.Trigger().OperandSizes());
     pTargetSizeColumn->SetHeader(L"Size");
     pTargetSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
     pTargetSizeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(7, std::move(pTargetSizeColumn));
 
-    auto pTargetValueColumn = std::make_unique<ValueColumnBinding>(
+    auto pTargetValueColumn = std::make_unique<TargetValueColumnBinding>(
         TriggerConditionViewModel::TargetValueProperty, TriggerConditionViewModel::TargetTypeProperty);
     pTargetValueColumn->SetHeader(L"Mem/Val");
     pTargetValueColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_VALUE);
diff --git a/src/ui/win32/bindings/GridLookupColumnBinding.cpp b/src/ui/win32/bindings/GridLookupColumnBinding.cpp
index 7a442e0a..a5096f3c 100644
--- a/src/ui/win32/bindings/GridLookupColumnBinding.cpp
+++ b/src/ui/win32/bindings/GridLookupColumnBinding.cpp
@@ -90,13 +90,30 @@ HWND GridLookupColumnBinding::CreateInPlaceEditor(HWND hParent, InPlaceEditorInf
 
     const auto& pItems = static_cast<GridBinding*>(pInfo.pGridBinding)->GetItems();
     const auto nValue = pItems.GetItemValue(pInfo.nItemIndex, *m_pBoundProperty);
+
+    const ViewModelBase* vmItem = nullptr;
+    if (m_fIsVisible)
+        vmItem = pItems.GetViewModelAt(pInfo.nItemIndex);
+
+    m_vVisibleItems.clear();
+
     for (size_t i = 0; i < m_vmItems.Count(); ++i)
     {
         const auto* pItem = m_vmItems.GetItemAt(i);
         Expects(pItem != nullptr);
+        const auto nItemValue = pItem->GetId();
+
+        if (vmItem != nullptr)
+        {
+            if (nItemValue != nValue && !m_fIsVisible(*vmItem, nItemValue))
+                continue;
+
+            m_vVisibleItems.push_back(nItemValue);
+        }
+
         const auto nIndex = ComboBox_AddString(hInPlaceEditor, NativeStr(pItem->GetLabel()).c_str());
 
-        if (pItem->GetId() == nValue)
+        if (nItemValue == nValue)
             ComboBox_SetCurSel(hInPlaceEditor, nIndex);
     }
 
@@ -110,6 +127,9 @@ HWND GridLookupColumnBinding::CreateInPlaceEditor(HWND hParent, InPlaceEditorInf
 
 int GridLookupColumnBinding::GetValueFromIndex(gsl::index nIndex) const
 {
+    if (m_fIsVisible != nullptr)
+        return m_vVisibleItems.at(nIndex);
+
     const auto* pItem = m_vmItems.GetItemAt(nIndex);
     return (pItem != nullptr) ? pItem->GetId() : 0;        
 }
diff --git a/src/ui/win32/bindings/GridLookupColumnBinding.hh b/src/ui/win32/bindings/GridLookupColumnBinding.hh
index e211d1a0..9e818f86 100644
--- a/src/ui/win32/bindings/GridLookupColumnBinding.hh
+++ b/src/ui/win32/bindings/GridLookupColumnBinding.hh
@@ -34,9 +34,15 @@ public:
     int GetValueFromIndex(gsl::index nIndex) const;
     const IntModelProperty& GetBoundProperty() const noexcept { return *m_pBoundProperty; }
 
+    typedef bool (*IsVisibleFunction)(const ViewModelBase& vmItem, int nValue);
+    void SetVisibilityFilter(IsVisibleFunction fIsVisible) noexcept { m_fIsVisible = fIsVisible; }
+
 protected:
     const IntModelProperty* m_pBoundProperty = nullptr;
     const ra::ui::viewmodels::LookupItemViewModelCollection& m_vmItems;
+
+    IsVisibleFunction m_fIsVisible = nullptr;
+    std::vector<int> m_vVisibleItems;
 };
 
 } // namespace bindings


Test patch:
diff --git a/tests/ui/ViewModelCollection_Tests.cpp b/tests/ui/ViewModelCollection_Tests.cpp
index c3079f6f..25b4d79b 100644
--- a/tests/ui/ViewModelCollection_Tests.cpp
+++ b/tests/ui/ViewModelCollection_Tests.cpp
@@ -249,6 +249,28 @@ public:
         Assert::AreEqual(TestViewModel::BoolProperty.GetDefaultValue(), vmCollection.GetItemValue(1, TestViewModel::BoolProperty));
     }
 
+    TEST_METHOD(TestGetViewModelAt)
+    {
+        ViewModelCollection<TestViewModel> vmCollection;
+        auto& pItem1 = vmCollection.Add(1, L"Test1");
+        pItem1.SetBool(false);
+
+        auto& pItem2 = vmCollection.Add(2, L"Test2");
+        pItem2.SetBool(true);
+
+        const auto* vm1 = vmCollection.GetViewModelAt(0);
+        Assert::IsTrue(vm1 == &pItem1);
+
+        const auto* vm2 = vmCollection.GetViewModelAt(1);
+        Assert::IsTrue(vm2 == &pItem2);
+
+        const auto* vm3 = vmCollection.GetViewModelAt(2);
+        Assert::IsNull(vm3);
+
+        const auto* vm0 = vmCollection.GetViewModelAt(-1);
+        Assert::IsNull(vm0);
+    }
+
     TEST_METHOD(TestStringPropertyNotification)
     {
         ViewModelCollection<TestViewModel> vmCollection;
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index a2dfbc48..804b0b02 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -130,6 +130,11 @@ private:
             TriggerConditionViewModel::InitializeFrom(*pTrigger->requirement->conditions);
         }
 
+        bool HasSourceSize() const { return GetValue(HasSourceSizeProperty); }
+        bool HasTargetSize() const { return GetValue(HasTargetSizeProperty); }
+        bool HasTarget() const { return GetValue(HasTargetProperty); }
+        bool HasHits() const { return GetValue(HasHitsProperty); }
+
     private:
         std::string sBuffer;
     };
@@ -154,7 +159,7 @@ private:
 public:
     TEST_METHOD(TestInitialState)
     {
-        TriggerConditionViewModel vmCondition;
+        TriggerConditionViewModelHarness vmCondition;
         Assert::AreEqual(1, vmCondition.GetIndex());
         Assert::AreEqual(TriggerConditionType::Standard, vmCondition.GetType());
         Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
@@ -166,6 +171,204 @@ public:
         Assert::AreEqual({ 0U }, vmCondition.GetTargetValue());
         Assert::AreEqual(0U, vmCondition.GetCurrentHits());
         Assert::AreEqual(0U, vmCondition.GetRequiredHits());
+
+        Assert::IsTrue(vmCondition.HasSourceSize());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+        Assert::IsTrue(vmCondition.HasHits());
+    }
+
+    TEST_METHOD(TestHasSourceSize)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::BCD);
+        Assert::AreEqual(TriggerOperandType::BCD, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Delta);
+        Assert::AreEqual(TriggerOperandType::Delta, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Prior);
+        Assert::AreEqual(TriggerOperandType::Prior, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Value);
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetSourceType());
+        Assert::IsFalse(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Address);
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+    }
+
+    TEST_METHOD(TestHasTarget)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperatorType::Equals, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::NotEquals);
+        Assert::AreEqual(TriggerOperatorType::NotEquals, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::LessThan);
+        Assert::AreEqual(TriggerOperatorType::LessThan, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::LessThanOrEqual);
+        Assert::AreEqual(TriggerOperatorType::LessThanOrEqual, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::GreaterThan);
+        Assert::AreEqual(TriggerOperatorType::GreaterThan, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::GreaterThanOrEqual);
+        Assert::AreEqual(TriggerOperatorType::GreaterThanOrEqual, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::AreEqual(TriggerOperatorType::None, vmCondition.GetOperator());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Multiply);
+        Assert::AreEqual(TriggerOperatorType::Multiply, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Divide);
+        Assert::AreEqual(TriggerOperatorType::Divide, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::BitwiseAnd);
+        Assert::AreEqual(TriggerOperatorType::BitwiseAnd, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+    }
+
+    TEST_METHOD(TestHasTargetSize)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::AreEqual(TriggerOperatorType::Equals, vmCondition.GetOperator());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Address);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Delta);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Equals);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Prior);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::NotEquals);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+    }
+
+    TEST_METHOD(TestHasTargetSizeHasTarget)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Address);
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::BCD);
+        Assert::AreEqual(TriggerOperandType::BCD, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Delta);
+        Assert::AreEqual(TriggerOperandType::Delta, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Prior);
+        Assert::AreEqual(TriggerOperandType::Prior, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+    }
+
+    TEST_METHOD(TestHasHits)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerConditionType::Standard, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddAddress);
+        Assert::AreEqual(TriggerConditionType::AddAddress, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddHits);
+        Assert::AreEqual(TriggerConditionType::AddHits, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddSource);
+        Assert::AreEqual(TriggerConditionType::AddSource, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AndNext);
+        Assert::AreEqual(TriggerConditionType::AndNext, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::Measured);
+        Assert::AreEqual(TriggerConditionType::Measured, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::MeasuredIf);
+        Assert::AreEqual(TriggerConditionType::MeasuredIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::OrNext);
+        Assert::AreEqual(TriggerConditionType::OrNext, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::PauseIf);
+        Assert::AreEqual(TriggerConditionType::PauseIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::ResetIf);
+        Assert::AreEqual(TriggerConditionType::ResetIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::SubSource);
+        Assert::AreEqual(TriggerConditionType::SubSource, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::Trigger);
+        Assert::AreEqual(TriggerConditionType::Trigger, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
     }
 
     TEST_METHOD(TestSizes)
@@ -204,6 +407,7 @@ public:
         ParseAndRegenerate("0xH1234<=5"); // less than or equal
         ParseAndRegenerate("0xH1234>5"); // greater than
         ParseAndRegenerate("0xH1234>=5"); // greater than or equal
+        ParseAndRegenerate("A:0xH1234"); // none
         ParseAndRegenerate("A:0xH1234*5"); // multiply
         ParseAndRegenerate("A:0xH1234/5"); // divide
         ParseAndRegenerate("A:0xH1234&5"); // bitwise and
@@ -297,6 +501,114 @@ public:
         Assert::AreEqual(std::wstring(L"255"), condition.GetTooltip(TriggerConditionViewModel::SourceValueProperty));
         Assert::AreEqual(std::wstring(L"99"), condition.GetTooltip(TriggerConditionViewModel::TargetValueProperty));
     }
+
+    TEST_METHOD(TestIsModifying)
+    {
+        TriggerConditionViewModelHarness condition;
+
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AddHits);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AddSource);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AndNext);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Measured);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::MeasuredIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::OrNext);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::PauseIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::ResetIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::SubSource);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Trigger);
+        Assert::IsFalse(condition.IsModifying());
+    }
+
+    TEST_METHOD(TestSwitchBetweenModifyingAndNonModifying)
+    {
+        TriggerConditionViewModelHarness condition;
+        condition.SetSourceValue(0x1234U);
+        condition.SetOperator(TriggerOperatorType::NotEquals);
+        condition.SetTargetValue(8);
+        Assert::AreEqual(std::string("0xH1234!=8"), condition.Serialize());
+        Assert::IsFalse(condition.IsModifying());
+
+        // change to modifying switches operator to None. the 8 is unmodified, but ignored
+        condition.SetType(TriggerConditionType::AddSource);
+        Assert::AreEqual(TriggerOperatorType::None, condition.GetOperator());
+        Assert::AreEqual(std::string("A:0xH1234"), condition.Serialize());
+        Assert::IsTrue(condition.IsModifying());
+
+        // changing operator "restores" the 8
+        condition.SetOperator(TriggerOperatorType::Multiply);
+        Assert::AreEqual(std::string("A:0xH1234*8"), condition.Serialize());
+
+        // change to non-modifying switches operator to Equals
+        condition.SetType(TriggerConditionType::ResetIf);
+        Assert::AreEqual(TriggerOperatorType::Equals, condition.GetOperator());
+        Assert::AreEqual(std::string("R:0xH1234=8"), condition.Serialize());
+        Assert::IsFalse(condition.IsModifying());
+
+        // change to modifying switches operator to None. the 8 is unmodified, but ignored
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::AreEqual(TriggerOperatorType::None, condition.GetOperator());
+        Assert::AreEqual(std::string("I:0xH1234"), condition.Serialize());
+        Assert::IsTrue(condition.IsModifying());
+
+        // change to non-modifying switches operator to Equals
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::AreEqual(TriggerOperatorType::Equals, condition.GetOperator());
+        Assert::AreEqual(std::string("0xH1234=8"), condition.Serialize());
+        Assert::IsFalse(condition.IsModifying());
+    }
+
+    TEST_METHOD(TestIsComparisonVisible)
+    {
+        TriggerConditionViewModelHarness condition;
+
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Equals)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::NotEquals)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThan)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThanOrEqual)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThan)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThanOrEqual)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::None)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Multiply)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
+
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Equals)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::NotEquals)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThan)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThanOrEqual)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThan)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThanOrEqual)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::None)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Multiply)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
+    }
 };
 
 } // namespace tests