Repository: RetroAchievements/RAIntegration
Pull request #616 (2020-10-11 00:47:02)

bind trigger to asset editor dialog


Patch:
diff --git a/src/RA_Core.cpp b/src/RA_Core.cpp
index ea629043..63c25f85 100644
--- a/src/RA_Core.cpp
+++ b/src/RA_Core.cpp
@@ -168,6 +168,9 @@ API int CCONV _RA_Shutdown()
         if (pWindowManager.MemoryBookmarks.IsVisible())
             pDesktop.CloseWindow(pWindowManager.MemoryBookmarks);
 
+        if (pWindowManager.AssetEditor.IsVisible())
+            pDesktop.CloseWindow(pWindowManager.AssetEditor);
+
         if (pWindowManager.AssetList.IsVisible())
             pDesktop.CloseWindow(pWindowManager.AssetList);
 
diff --git a/src/RA_Dlg_AchEditor.cpp b/src/RA_Dlg_AchEditor.cpp
index b98c30d8..f09c798e 100644
--- a/src/RA_Dlg_AchEditor.cpp
+++ b/src/RA_Dlg_AchEditor.cpp
@@ -811,6 +811,12 @@ INT_PTR Dlg_AchievementEditor::AchievementEditorProc(HWND hDlg, UINT uMsg, WPARA
                 m_pListViewWndProc = SubclassWindow(hList, ListViewWndProc);
             }
 
+            LV_COLUMN col{};
+            col.mask = LVCF_WIDTH | LVCF_FMT;
+            col.fmt = LVCFMT_FIXED_WIDTH | LVCFMT_LEFT;
+            col.cx = 50;
+            ListView_InsertColumn(GetDlgItem(hDlg, IDC_RA_ACH_GROUP), 0, &col);
+
             RestoreWindowPosition(hDlg, "Achievement Editor", true, true);
         }
             return TRUE;
@@ -1421,8 +1427,7 @@ INT_PTR Dlg_AchievementEditor::AchievementEditorProc(HWND hDlg, UINT uMsg, WPARA
                     if (pActiveAchievement == nullptr)
                         break;
 
-                    const HWND hGroupList = GetDlgItem(m_hAchievementEditorDlg, IDC_RA_ACH_GROUP);
-                    const auto nSel = ListBox_GetCurSel(hGroupList);
+                    auto nSel = GetSelectedConditionGroup();
 
                     if (nSel == 0)
                     {
@@ -1442,7 +1447,11 @@ INT_PTR Dlg_AchievementEditor::AchievementEditorProc(HWND hDlg, UINT uMsg, WPARA
                     }
 
                     pActiveAchievement->RemoveAltGroup(gsl::narrow_cast<gsl::index>(nSel) - 1);
-                    ListBox_SetCurSel(hGroupList, nSel == ra::to_signed(pActiveAchievement->NumConditionGroups()) ? nSel - 1 : nSel);
+
+                    if (nSel == pActiveAchievement->NumConditionGroups())
+                        --nSel;
+
+                    SetSelectedConditionGroup(nSel);
 
                     RepopulateGroupList(pActiveAchievement);
                     pActiveAchievement->RebuildTrigger();
@@ -1450,14 +1459,6 @@ INT_PTR Dlg_AchievementEditor::AchievementEditorProc(HWND hDlg, UINT uMsg, WPARA
                     g_AchievementsDialog.OnEditAchievement(*pActiveAchievement);
                 }
                 break;
-                case IDC_RA_ACH_GROUP:
-                    switch (HIWORD(wParam))
-                    {
-                        case LBN_SELCHANGE:
-                            PopulateConditions(ActiveAchievement());
-                            break;
-                    }
-                    break;
             }
 
             // Switch also on the highword:
@@ -1764,6 +1765,15 @@ INT_PTR Dlg_AchievementEditor::AchievementEditorProc(HWND hDlg, UINT uMsg, WPARA
                 }
                 break;
 
+                case LVN_ITEMCHANGED:
+                {
+                    LPNMLISTVIEW pnmListView;
+                    GSL_SUPPRESS_TYPE1{ pnmListView = reinterpret_cast<LPNMLISTVIEW>(lParam); }
+                    if (pnmListView->uNewState & LVIS_SELECTED)
+                        PopulateConditions(ActiveAchievement());
+                }
+                break;
+
                 case TTN_GETDISPINFO:
                 {
                     LPNMTTDISPINFO lpDispInfo{};
@@ -2047,33 +2057,49 @@ _Use_decl_annotations_ void
     if (hGroupList == nullptr)
         return;
 
-    const int nSel = ListBox_GetCurSel(hGroupList);
+    int nItems = ListView_GetItemCount(hGroupList);
+
+    int nSel = -1;
+    for (int i = 0; i < nItems; ++i)
+    {
+        if (ListView_GetItemState(hGroupList, i, LVIS_SELECTED) == LVIS_SELECTED)
+        {
+            nSel = i;
+            break;
+        }
+    }
 
-    int nItems = ListBox_GetCount(hGroupList);
     const int nGroups = pCheevo ? gsl::narrow_cast<int>(pCheevo->NumConditionGroups()) : 0;
 
     while (nItems > nGroups)
-        ListBox_DeleteString(hGroupList, --nItems);
+        ListView_DeleteItem(hGroupList, --nItems);
 
     if (nItems < nGroups)
     {
+        LV_ITEM item{};
+        item.mask = LVIF_TEXT;
+        item.iSubItem = 0;
+
         if (nItems == 0)
         {
-            ListBox_AddString(hGroupList, _T("Core"));
+            std::string sCore = "Core";
+            item.pszText = sCore.data();
+            item.iItem = 0;
+            ListView_InsertItem(hGroupList, &item);
             ++nItems;
         }
 
         while (nItems < nGroups)
         {
-            const auto sAltGroup = ra::StringPrintf("Alt %02d", nItems++);
-            ListBox_AddString(hGroupList, NativeStr(sAltGroup).c_str());
+            auto sAltGroup = ra::StringPrintf("Alt %02d", nItems);
+            item.pszText = sAltGroup.data();
+            item.iItem = nItems++;
+            ListView_InsertItem(hGroupList, &item);
         }
     }
 
-    if (nSel < 0)
-        ListBox_SetCurSel(hGroupList, 0);
-    else if (nSel >= nGroups)
-        ListBox_SetCurSel(hGroupList, static_cast<size_t>(nGroups) - 1);
+    for (int i = 0; i < nItems; ++i)
+        ListView_SetItemState(hGroupList, i, (i == nSel) ? LVIS_SELECTED : 0, LVIS_SELECTED);
 }
 
 _Use_decl_annotations_ void Dlg_AchievementEditor::PopulateConditions(const Achievement* const restrict pCheevo)
@@ -2302,20 +2328,23 @@ void Dlg_AchievementEditor::OnLoad_NewRom()
 size_t Dlg_AchievementEditor::GetSelectedConditionGroup() const noexcept
 {
     HWND hList = GetDlgItem(g_AchievementEditorDialog.GetHWND(), IDC_RA_ACH_GROUP);
-    const int nSel = ListBox_GetCurSel(hList);
-    if (nSel == LB_ERR)
+    const int nItems = ListView_GetItemCount(hList);
+    for (int i = 0; i < nItems; ++i)
     {
-        OutputDebugString(TEXT("ListBox_GetCurSel returning LB_ERR\n"));
-        return 0;
+        if (ListView_GetItemState(hList, i, LVIS_SELECTED) == LVIS_SELECTED)
+            return ra::to_unsigned(i);
     }
 
-    return ra::to_unsigned(nSel);
+    OutputDebugString(TEXT("ListBox_GetCurSel returning LB_ERR\n"));
+    return 0;
 }
 
 void Dlg_AchievementEditor::SetSelectedConditionGroup(size_t nGrp) const noexcept
 {
     HWND hList = GetDlgItem(g_AchievementEditorDialog.GetHWND(), IDC_RA_ACH_GROUP);
-    ListBox_SetCurSel(hList, ra::to_signed(nGrp));
+    const int nItems = ListView_GetItemCount(hList);
+    for (size_t i = 0; i < gsl::narrow_cast<size_t>(nItems); ++i)
+        ListView_SetItemState(hList, i, (i == nGrp) ? LVIS_SELECTED : 0, LVIS_SELECTED);
 }
 
 void GenerateResizes(HWND hDlg)
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index e4ad8da4..28043cc6 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -136,7 +136,7 @@ BEGIN
     CONTROL         "",IDC_RA_CHEEVOPIC,"Static",SS_BITMAP | SS_CENTERIMAGE,384,7,53,49,WS_EX_CLIENTEDGE
     LTEXT           "Requirements:",IDC_STATIC,7,60,48,8
     LTEXT           "Group:",IDC_STATIC,7,74,23,8
-    LISTBOX         IDC_RA_ACH_GROUP,7,87,39,67,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
+    CONTROL         "",IDC_RA_ACH_GROUP,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOCOLUMNHEADER | WS_BORDER | WS_TABSTOP,7,87,39,67
     PUSHBUTTON      "+",IDC_RA_ACH_ADDGROUP,7,159,15,16,BS_MULTILINE
     PUSHBUTTON      "-",IDC_RA_ACH_DELGROUP,31,159,15,16,BS_MULTILINE
     CONTROL         "Pause on Reset",IDC_RA_CHK_ACH_PAUSE_ON_RESET,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,268,63,65,10
diff --git a/src/data/GameContext.cpp b/src/data/GameContext.cpp
index a35702f6..239581a5 100644
--- a/src/data/GameContext.cpp
+++ b/src/data/GameContext.cpp
@@ -66,12 +66,14 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
     m_vAchievements.clear();
     m_vLeaderboards.clear();
 
-    auto& vmAssets = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetList;
+    auto& vmWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
+    vmWindowManager.AssetEditor.LoadAsset(nullptr);
+
+    auto& vmAssets = vmWindowManager.AssetList;
     vmAssets.SetGameId(nGameId);
     vmAssets.Assets().BeginUpdate();
 
-    for (gsl::index nIndex = vmAssets.Assets().Count() - 1; nIndex >= 0; --nIndex)
-        vmAssets.Assets().RemoveAt(nIndex);
+    vmAssets.Assets().Clear();
 
     if (nGameId == 0)
     {
diff --git a/src/ui/ViewModelCollection.cpp b/src/ui/ViewModelCollection.cpp
index 662a8c06..e28da08c 100644
--- a/src/ui/ViewModelCollection.cpp
+++ b/src/ui/ViewModelCollection.cpp
@@ -47,6 +47,30 @@ void ViewModelCollectionBase::RemoveAt(gsl::index nIndex)
     }
 }
 
+void ViewModelCollectionBase::Clear()
+{
+    assert(!IsFrozen());
+
+    if (m_nSize == 0)
+        return;
+
+    BeginUpdate();
+
+    const bool bIsWatching = IsWatching();
+    for (size_t nIndex = 0; nIndex < m_nSize; ++nIndex)
+    {
+        auto& pItem = m_vItems.at(nIndex);
+        if (bIsWatching)
+            pItem.StopWatching();
+
+        pItem.AttachViewModel(nullptr);
+    }
+
+    m_nSize = 0;
+
+    EndUpdate();
+}
+
 void ViewModelCollectionBase::MoveItem(gsl::index nIndex, gsl::index nNewIndex)
 {
     // nothing to do
diff --git a/src/ui/ViewModelCollection.hh b/src/ui/ViewModelCollection.hh
index e1d3741c..fd6072a8 100644
--- a/src/ui/ViewModelCollection.hh
+++ b/src/ui/ViewModelCollection.hh
@@ -118,6 +118,11 @@ public:
     /// </summary>
     void RemoveAt(gsl::index nIndex);
 
+    /// <summary>
+    /// Removes all items from the collection.
+    /// </summary>
+    void Clear();
+
     /// <summary>
     /// Moves an item from one index to another.
     /// </summary>
diff --git a/src/ui/viewmodels/AssetEditorViewModel.cpp b/src/ui/viewmodels/AssetEditorViewModel.cpp
index 590532af..882d94e5 100644
--- a/src/ui/viewmodels/AssetEditorViewModel.cpp
+++ b/src/ui/viewmodels/AssetEditorViewModel.cpp
@@ -1,5 +1,8 @@
 #include "AssetEditorViewModel.hh"
 
+#include "services\AchievementRuntime.hh"
+#include "services\ServiceLocator.hh"
+
 #include "ui\viewmodels\MessageBoxViewModel.hh"
 
 namespace ra {
@@ -20,6 +23,8 @@ const BoolModelProperty AssetEditorViewModel::AssetLoadedProperty("AssetEditorVi
 AssetEditorViewModel::AssetEditorViewModel() noexcept
 {
     SetWindowTitle(L"Asset Editor");
+
+    m_vmTrigger.AddNotifyTarget(*this);
 }
 
 AssetEditorViewModel::~AssetEditorViewModel()
@@ -67,6 +72,8 @@ void AssetEditorViewModel::LoadAsset(AssetViewModelBase* pAsset)
             SetPauseOnReset(pAchievement->IsPauseOnReset());
             SetPauseOnTrigger(pAchievement->IsPauseOnTrigger());
             SetWindowTitle(L"Achievement Editor");
+
+            Trigger().InitializeFrom(pAchievement->GetTrigger());
         }
 
         SetValue(AssetLoadedProperty, true);
@@ -108,7 +115,9 @@ void AssetEditorViewModel::OnViewModelStringValueChanged(const StringModelProper
 
 void AssetEditorViewModel::OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs& args)
 {
-    if (args.Property == AssetViewModelBase::StateProperty)
+    if (args.Property == TriggerViewModel::VersionProperty)
+        OnTriggerChanged();
+    else if (args.Property == AssetViewModelBase::StateProperty)
         SetState(ra::itoe<AssetState>(args.tNewValue));
     else if (args.Property == AssetViewModelBase::CategoryProperty)
         SetCategory(ra::itoe<AssetCategory>(args.tNewValue));
@@ -160,7 +169,15 @@ void AssetEditorViewModel::OnValueChanged(const StringModelProperty::ChangeArgs&
 
 void AssetEditorViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
 {
-    if (m_pAsset != nullptr)
+    if (args.Property == DialogResultProperty)
+    {
+        if (m_pAsset)
+        {
+            m_pAsset->RemoveNotifyTarget(*this);
+            m_pAsset = nullptr;
+        }
+    }
+    else if (m_pAsset != nullptr)
     {
         if (args.Property == StateProperty)
         {
@@ -184,6 +201,11 @@ void AssetEditorViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& ar
     WindowViewModelBase::OnValueChanged(args);
 }
 
+void AssetEditorViewModel::OnTriggerChanged() noexcept
+{
+    // TODO: update asset / runtime
+}
+
 } // namespace viewmodels
 } // namespace ui
 } // namespace ra
diff --git a/src/ui/viewmodels/AssetEditorViewModel.hh b/src/ui/viewmodels/AssetEditorViewModel.hh
index c831b1dd..2863e1d0 100644
--- a/src/ui/viewmodels/AssetEditorViewModel.hh
+++ b/src/ui/viewmodels/AssetEditorViewModel.hh
@@ -2,14 +2,14 @@
 #define RA_UI_ASSETEDITOR_VIEW_MODEL_H
 #pragma once
 
-#include "ui\WindowViewModelBase.hh"
-
 #include "AssetViewModelBase.hh"
-
 #include "AchievementViewModel.hh"
+#include "TriggerViewModel.hh"
 
 #include "data\Types.hh"
 
+#include "ui\WindowViewModelBase.hh"
+
 namespace ra {
 namespace ui {
 namespace viewmodels {
@@ -180,6 +180,9 @@ public:
 
     void LoadAsset(AssetViewModelBase* pAsset);
 
+    TriggerViewModel& Trigger() noexcept { return m_vmTrigger; }
+    const TriggerViewModel& Trigger() const noexcept { return m_vmTrigger; }
+
 protected:
     // ViewModelBase::NotifyTarget
     void OnViewModelBoolValueChanged(const BoolModelProperty::ChangeArgs& args) override;
@@ -190,6 +193,10 @@ protected:
     void OnValueChanged(const StringModelProperty::ChangeArgs& args) override;
     void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
 
+    void OnTriggerChanged() noexcept;
+
+    TriggerViewModel m_vmTrigger;
+
     AssetViewModelBase* m_pAsset = nullptr;
 };
 
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index cf4dd70c..95a5f9b7 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -57,6 +57,11 @@ AssetListViewModel::AssetListViewModel() noexcept
     m_vFilteredAssets.AddNotifyTarget(m_pFilteredListMonitor);
 }
 
+AssetListViewModel::~AssetListViewModel()
+{
+    m_bNeedToUpdateButtons.store(false);
+}
+
 static bool IsTalliedAchievement(const AchievementViewModel& pAchievement)
 {
     return (pAchievement.GetCategory() == AssetCategory::Core);
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index e8ca4d50..f3049ba6 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -21,7 +21,7 @@ class AssetListViewModel : public WindowViewModelBase,
 {
 public:
     GSL_SUPPRESS_F6 AssetListViewModel() noexcept;
-    ~AssetListViewModel() = default;
+    ~AssetListViewModel();
 
     AssetListViewModel(const AssetListViewModel&) noexcept = delete;
     AssetListViewModel& operator=(const AssetListViewModel&) noexcept = delete;
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 0874d5c0..8e4b29a6 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -134,10 +134,6 @@ void TriggerConditionViewModel::SerializeAppendOperand(std::string& sBuffer, Tri
             sBuffer.push_back('b');
             break;
 
-        case TriggerOperandType::Inverted:
-            sBuffer.push_back('~');
-            break;
-
         default:
             assert(!"Unknown operand type");
             break;
@@ -212,7 +208,6 @@ void TriggerConditionViewModel::SetOperand(const IntModelProperty& pTypeProperty
         case TriggerOperandType::Delta:
         case TriggerOperandType::Prior:
         case TriggerOperandType::BCD:
-        case TriggerOperandType::Inverted:
         {
             const auto nSize = MapMemSize(operand.size);
             SetValue(pSizeProperty, ra::etoi(nSize));
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index f71ad381..e95b7258 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -37,7 +37,6 @@ enum class TriggerOperandType : uint8_t
     Value = RC_OPERAND_CONST,           // a 32 bit unsigned integer
     Prior = RC_OPERAND_PRIOR,           // the last differing value at this address.
     BCD = RC_OPERAND_BCD,               // Address, but decoded from binary-coded-decimal
-    Inverted = RC_OPERAND_INVERTED      // Address, but two-compliment applied (~Address)
 };
 
 enum class TriggerOperatorType : uint8_t
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index 12b509e9..fd8bf3f0 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -11,71 +11,110 @@ const IntModelProperty TriggerViewModel::SelectedGroupIndexProperty("TriggerView
 const BoolModelProperty TriggerViewModel::PauseOnResetProperty("TriggerViewModel", "PauseOnReset", false);
 const BoolModelProperty TriggerViewModel::PauseOnTriggerProperty("TriggerViewModel", "PauseOnTrigger", false);
 const IntModelProperty TriggerViewModel::VersionProperty("TriggerViewModel", "Version", 0);
-const IntModelProperty TriggerViewModel::GroupViewModel::VersionProperty("GroupViewModel", "Version", 0);
 
-std::string TriggerViewModel::GroupViewModel::Serialize() const
+TriggerViewModel::TriggerViewModel() noexcept
+    : m_pConditionsMonitor(*this)
+{
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Standard), L"");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::PauseIf), L"Pause If");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::ResetIf), L"Reset If");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AddSource), L"Add Source");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::SubSource), L"Sub Source");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AddHits), L"Add Hits");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AddAddress), L"Add Address");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AndNext), L"And Next");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::OrNext), L"Or Next");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Measured), L"Measured");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::MeasuredIf), L"Measured If");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Trigger), L"Trigger");
+
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Address), L"Mem");
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Value), L"Value");
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Delta), L"Delta");
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Prior), L"Prior");
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::BCD), L"BCD");
+
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_0), L"Bit0");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_1), L"Bit1");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_2), L"Bit2");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_3), L"Bit3");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_4), L"Bit4");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_5), L"Bit5");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_6), L"Bit6");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Bit_7), L"Bit7");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Nibble_Lower), L"Lower4");
+    m_vOperandSizes.Add(ra::etoi(MemSize::Nibble_Upper), L"Upper4");
+    m_vOperandSizes.Add(ra::etoi(MemSize::EightBit), L"8-bit");
+    m_vOperandSizes.Add(ra::etoi(MemSize::SixteenBit), L"16-bit");
+    m_vOperandSizes.Add(ra::etoi(MemSize::TwentyFourBit), L"24-bit");
+    m_vOperandSizes.Add(ra::etoi(MemSize::ThirtyTwoBit), L"32-bit");
+    m_vOperandSizes.Add(ra::etoi(MemSize::BitCount), L"BitCount");
+
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Equals), L"=");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::LessThan), L"<");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::LessThanOrEqual), L"<=");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::GreaterThan), L">");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::GreaterThanOrEqual), L">=");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::NotEquals), L"!=");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::None), L"");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Multiply), L"*");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Divide), L"/");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::BitwiseAnd), L"&");
+}
+
+std::string TriggerViewModel::Serialize() const
 {
     std::string buffer;
     SerializeAppend(buffer);
     return buffer;
 }
 
-void TriggerViewModel::GroupViewModel::SerializeAppend(std::string& sBuffer) const
+bool TriggerViewModel::GroupViewModel::UpdateSerialized(const ViewModelCollection<TriggerConditionViewModel>& vConditions)
 {
-    for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(m_vConditions.Count()); ++nIndex)
+    std::string sSerialized;
+    UpdateSerialized(sSerialized, vConditions);
+
+    if (sSerialized != m_sSerialized)
     {
-        const auto* vmCondition = m_vConditions.GetItemAt(nIndex);
-        if (vmCondition != nullptr)
-        {
-            vmCondition->SerializeAppend(sBuffer);
-            sBuffer.push_back('_');
-        }
+        m_sSerialized.swap(sSerialized);
+        return true;
     }
 
-    if (!sBuffer.empty() && sBuffer.back() == '_')
-        sBuffer.pop_back();
+    return false;
 }
 
-void TriggerViewModel::GroupViewModel::InitializeFrom(const rc_condset_t& pConditionSet)
+void TriggerViewModel::GroupViewModel::UpdateSerialized(std::string& sSerialized, const ViewModelCollection<TriggerConditionViewModel>& vConditions)
 {
-    m_vConditions.RemoveNotifyTarget(*this);
-
-    rc_condition_t* pCondition = pConditionSet.conditions;
-    for (; pCondition != nullptr; pCondition = pCondition->next)
+    for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(vConditions.Count()); ++nIndex)
     {
-        auto& vmCondition = m_vConditions.Add();
-        vmCondition.SetIndex(gsl::narrow_cast<int>(m_vConditions.Count()));
-        vmCondition.InitializeFrom(*pCondition);
+        const auto* vmCondition = vConditions.GetItemAt(nIndex);
+        if (vmCondition != nullptr)
+        {
+            vmCondition->SerializeAppend(sSerialized);
+            sSerialized.push_back('_');
+        }
     }
 
-    m_vConditions.AddNotifyTarget(*this);
-}
-
-void TriggerViewModel::GroupViewModel::OnViewModelIntValueChanged(gsl::index, const IntModelProperty::ChangeArgs&)
-{
-    UpdateVersion();
-}
-
-void TriggerViewModel::GroupViewModel::OnViewModelAdded(gsl::index)
-{
-    UpdateVersion();
+    if (!sSerialized.empty() && sSerialized.back() == '_')
+        sSerialized.pop_back();
 }
 
-void TriggerViewModel::GroupViewModel::OnViewModelRemoved(gsl::index)
+const std::string& TriggerViewModel::GroupViewModel::GetSerialized() const
 {
-    UpdateVersion();
-}
+    if (m_sSerialized.empty() && m_pConditionSet)
+    {
+        ViewModelCollection<TriggerConditionViewModel> vConditions;
+        rc_condition_t* pCondition = m_pConditionSet->conditions;
+        for (; pCondition != nullptr; pCondition = pCondition->next)
+        {
+            auto& vmCondition = vConditions.Add();
+            vmCondition.InitializeFrom(*pCondition);
+        }
 
-void TriggerViewModel::GroupViewModel::UpdateVersion()
-{
-    SetValue(VersionProperty, GetValue(VersionProperty) + 1);
-}
+        UpdateSerialized(m_sSerialized, vConditions);
+    }
 
-std::string TriggerViewModel::Serialize() const
-{
-    std::string buffer;
-    SerializeAppend(buffer);
-    return buffer;
+    return m_sSerialized;
 }
 
 void TriggerViewModel::SerializeAppend(std::string& sBuffer) const
@@ -83,12 +122,12 @@ void TriggerViewModel::SerializeAppend(std::string& sBuffer) const
     for (gsl::index nGroup = 0; nGroup < gsl::narrow_cast<gsl::index>(m_vGroups.Count()); ++nGroup)
     {
         const auto* vmGroup = m_vGroups.GetItemAt(nGroup);
-        if (vmGroup != nullptr && vmGroup->Conditions().Count() != 0)
+        if (vmGroup != nullptr)
         {
             if (nGroup != 0)
                 sBuffer.push_back('S');
 
-            vmGroup->SerializeAppend(sBuffer);
+            sBuffer.append(vmGroup->GetSerialized());
         }
     }
 }
@@ -96,40 +135,94 @@ void TriggerViewModel::SerializeAppend(std::string& sBuffer) const
 void TriggerViewModel::InitializeFrom(const rc_trigger_t& pTrigger)
 {
     m_vGroups.RemoveNotifyTarget(*this);
+    m_vGroups.BeginUpdate();
+
+    // this will not update the conditions collection because OnValueChange ignores it when m_vGroups.IsUpdating
+    SetSelectedGroupIndex(0);
+
+    m_vGroups.Clear();
 
     auto& vmCoreGroup = m_vGroups.Add();
     vmCoreGroup.SetId(0);
     vmCoreGroup.SetLabel(L"Core");
+    vmCoreGroup.SetSelected(true);
     if (pTrigger.requirement)
-        vmCoreGroup.InitializeFrom(*pTrigger.requirement);
+        vmCoreGroup.m_pConditionSet = pTrigger.requirement;
 
-    const rc_condset_t* pConditionSet = pTrigger.alternative;
+    int nId = 0;
+    rc_condset_t* pConditionSet = pTrigger.alternative;
     for (; pConditionSet != nullptr; pConditionSet = pConditionSet->next)
     {
         auto& vmAltGroup = m_vGroups.Add();
-        const auto nId = gsl::narrow_cast<int>(m_vGroups.Count());
-        vmAltGroup.SetId(nId);
+        vmAltGroup.SetId(++nId);
         vmAltGroup.SetLabel(ra::StringPrintf(L"Alt %d", nId));
-        vmAltGroup.InitializeFrom(*pConditionSet);
+        vmAltGroup.m_pConditionSet = pConditionSet;
     }
 
+    m_vGroups.EndUpdate();
     m_vGroups.AddNotifyTarget(*this);
+
+    // forcibly update the conditions from the core group that was selected earlier
+    UpdateConditions(&vmCoreGroup);
 }
 
-void TriggerViewModel::OnViewModelIntValueChanged(gsl::index, const IntModelProperty::ChangeArgs& args)
+void TriggerViewModel::InitializeFrom(const std::string& sTrigger)
 {
-    if (args.Property == GroupViewModel::VersionProperty)
-        UpdateVersion();
+    const auto nSize = rc_trigger_size(sTrigger.c_str());
+    if (nSize > 0)
+    {
+        m_sTriggerBuffer.resize(nSize);
+        m_pTrigger = rc_parse_trigger(m_sTriggerBuffer.data(), sTrigger.c_str(), nullptr, 0);
+        InitializeFrom(*m_pTrigger);
+    }
+    else
+    {
+        rc_trigger_t pTrigger;
+        memset(&pTrigger, 0, sizeof(pTrigger));
+        InitializeFrom(pTrigger);
+    }
+}
+
+void TriggerViewModel::OnViewModelBoolValueChanged(gsl::index, const BoolModelProperty::ChangeArgs& args)
+{
+    if (args.Property == GroupViewModel::IsSelectedProperty)
+    {
+        for (gsl::index i = 0; i < ra::to_signed(m_vGroups.Count()); ++i)
+        {
+            if (m_vGroups.GetItemAt(i)->IsSelected())
+            {
+                SetSelectedGroupIndex(gsl::narrow_cast<int>(i));
+                return;
+            }
+        }
+
+        SetSelectedGroupIndex(-1);
+    }
+}
+
+void TriggerViewModel::ConditionsMonitor::OnViewModelIntValueChanged(gsl::index, const IntModelProperty::ChangeArgs&)
+{
+    UpdateCurrentGroup();
+}
+
+void TriggerViewModel::ConditionsMonitor::OnViewModelAdded(gsl::index)
+{
+    UpdateCurrentGroup();
 }
 
-void TriggerViewModel::OnViewModelAdded(gsl::index)
+void TriggerViewModel::ConditionsMonitor::OnViewModelRemoved(gsl::index)
 {
-    UpdateVersion();
+    UpdateCurrentGroup();
 }
 
-void TriggerViewModel::OnViewModelRemoved(gsl::index)
+void TriggerViewModel::ConditionsMonitor::UpdateCurrentGroup()
 {
-    UpdateVersion();
+    auto* pGroup = m_vmTrigger->m_vGroups.GetItemAt(m_vmTrigger->GetSelectedGroupIndex());
+    if (pGroup)
+    {
+        if (pGroup->UpdateSerialized(m_vmTrigger->m_vConditions))
+            m_vmTrigger->UpdateVersion();
+    }
 }
 
 void TriggerViewModel::UpdateVersion()
@@ -137,6 +230,58 @@ void TriggerViewModel::UpdateVersion()
     SetValue(VersionProperty, GetValue(VersionProperty) + 1);
 }
 
+void TriggerViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
+{
+    if (args.Property == SelectedGroupIndexProperty)
+    {
+        // ignore change caused in InitializeFrom
+        if (!m_vGroups.IsUpdating())
+        {
+            // disable notifications while updating the selected group to prevent re-entrancy
+            m_vGroups.RemoveNotifyTarget(*this);
+
+            auto* pGroup = m_vGroups.GetItemAt(args.tNewValue);
+            if (pGroup != nullptr) // may be null if out of range (i.e. -1)
+                pGroup->SetSelected(true);
+
+            if (args.tOldValue != -1)
+            {
+                auto* pOldGroup = m_vGroups.GetItemAt(args.tOldValue);
+                Expects(pOldGroup != nullptr);
+                pOldGroup->SetSelected(false);
+            }
+
+            m_vGroups.AddNotifyTarget(*this);
+
+            UpdateConditions(pGroup);
+        }
+    }
+
+    ViewModelBase::OnValueChanged(args);
+}
+
+void TriggerViewModel::UpdateConditions(const GroupViewModel* pGroup)
+{
+    m_vConditions.RemoveNotifyTarget(m_pConditionsMonitor);
+    m_vConditions.BeginUpdate();
+
+    m_vConditions.Clear();
+
+    if (pGroup != nullptr && pGroup->m_pConditionSet)
+    {
+        rc_condition_t* pCondition = pGroup->m_pConditionSet->conditions;
+        for (; pCondition != nullptr; pCondition = pCondition->next)
+        {
+            auto& vmCondition = m_vConditions.Add();
+            vmCondition.SetIndex(gsl::narrow_cast<int>(m_vConditions.Count()));
+            vmCondition.InitializeFrom(*pCondition);
+        }
+    }
+
+    m_vConditions.EndUpdate();
+    m_vConditions.AddNotifyTarget(m_pConditionsMonitor);
+}
+
 } // namespace viewmodels
 } // namespace ui
 } // namespace ra
diff --git a/src/ui/viewmodels/TriggerViewModel.hh b/src/ui/viewmodels/TriggerViewModel.hh
index 4abe5314..1c2f4501 100644
--- a/src/ui/viewmodels/TriggerViewModel.hh
+++ b/src/ui/viewmodels/TriggerViewModel.hh
@@ -18,6 +18,15 @@ class TriggerViewModel : public ViewModelBase,
     protected ViewModelCollectionBase::NotifyTarget
 {
 public:
+    GSL_SUPPRESS_F6 TriggerViewModel() noexcept;
+    ~TriggerViewModel() = default;
+
+    TriggerViewModel(const TriggerViewModel&) noexcept = delete;
+    TriggerViewModel& operator=(const TriggerViewModel&) noexcept = delete;
+    TriggerViewModel(TriggerViewModel&&) noexcept = delete;
+    TriggerViewModel& operator=(TriggerViewModel&&) noexcept = delete;
+
+
     static const IntModelProperty SelectedGroupIndexProperty;
     int GetSelectedGroupIndex() const { return GetValue(SelectedGroupIndexProperty); }
     void SetSelectedGroupIndex(int nValue) { SetValue(SelectedGroupIndexProperty, nValue); }
@@ -34,47 +43,103 @@ public:
         protected ViewModelCollectionBase::NotifyTarget
     {
     public:
-        ViewModelCollection<TriggerConditionViewModel>& Conditions() noexcept { return m_vConditions; }
-        const ViewModelCollection<TriggerConditionViewModel>& Conditions() const noexcept { return m_vConditions; }
-
-        std::string Serialize() const;
-        void SerializeAppend(std::string& sBuffer) const;
-
-        void InitializeFrom(const struct rc_condset_t& pConditionSet);
+        rc_condset_t* m_pConditionSet = nullptr;
 
-        static const IntModelProperty VersionProperty;
-
-    protected:
-        void OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args) override;
-        void OnViewModelAdded(gsl::index nIndex) override;
-        void OnViewModelRemoved(gsl::index nIndex) override;
+        bool UpdateSerialized(const ViewModelCollection<TriggerConditionViewModel>& vConditions);
+        const std::string& GetSerialized() const;
 
     private:
-        void UpdateVersion();
-
-        ViewModelCollection<TriggerConditionViewModel> m_vConditions;
+        static void UpdateSerialized(std::string& sSerialized, const ViewModelCollection<TriggerConditionViewModel>& vConditions);
+        mutable std::string m_sSerialized;
     };
 
     ViewModelCollection<GroupViewModel>& Groups() noexcept { return m_vGroups; }
     const ViewModelCollection<GroupViewModel>& Groups() const noexcept { return m_vGroups; }
 
+    ViewModelCollection<TriggerConditionViewModel>& Conditions() noexcept { return m_vConditions; }
+    const ViewModelCollection<TriggerConditionViewModel>& Conditions() const noexcept { return m_vConditions; }
+
     static const IntModelProperty VersionProperty;
 
     std::string Serialize() const;
     void SerializeAppend(std::string& sBuffer) const;
 
     void InitializeFrom(const rc_trigger_t& pTrigger);
+    void InitializeFrom(const std::string& sTrigger);
+
+    /// <summary>
+    /// Gets the list of condition types.
+    /// </summary>
+    const LookupItemViewModelCollection& ConditionTypes() const noexcept
+    {
+        return m_vConditionTypes;
+    }
+
+    /// <summary>
+    /// Gets the list of operand types.
+    /// </summary>
+    const LookupItemViewModelCollection& OperandTypes() const noexcept
+    {
+        return m_vOperandTypes;
+    }
+
+    /// <summary>
+    /// Gets the list of operand sizes.
+    /// </summary>
+    const LookupItemViewModelCollection& OperandSizes() const noexcept
+    {
+        return m_vOperandSizes;
+    }
+
+    /// <summary>
+    /// Gets the list of operator types.
+    /// </summary>
+    const LookupItemViewModelCollection& OperatorTypes() const noexcept
+    {
+        return m_vOperatorTypes;
+    }
+
 
 protected:
     // ViewModelCollectionBase::NotifyTarget
-    void OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args) override;
-    void OnViewModelAdded(gsl::index nIndex) override;
-    void OnViewModelRemoved(gsl::index nIndex) override;
+    void OnViewModelBoolValueChanged(gsl::index nIndex, const BoolModelProperty::ChangeArgs& args) override;
+
+    void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
 
 private:
     void UpdateVersion();
+    void UpdateConditions(const GroupViewModel* pGroup);
 
     ViewModelCollection<GroupViewModel> m_vGroups;
+    ViewModelCollection<TriggerConditionViewModel> m_vConditions;
+
+    class ConditionsMonitor : public ViewModelCollectionBase::NotifyTarget
+    {
+    public:
+        ConditionsMonitor(TriggerViewModel& vmTrigger) noexcept
+        {
+            m_vmTrigger = &vmTrigger;
+        }
+
+    protected:
+        void OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args) override;
+        void OnViewModelAdded(gsl::index nIndex) override;
+        void OnViewModelRemoved(gsl::index nIndex) override;
+
+        void UpdateCurrentGroup();
+
+        TriggerViewModel* m_vmTrigger;
+    };
+
+    ConditionsMonitor m_pConditionsMonitor;
+
+    LookupItemViewModelCollection m_vConditionTypes;
+    LookupItemViewModelCollection m_vOperandTypes;
+    LookupItemViewModelCollection m_vOperandSizes;
+    LookupItemViewModelCollection m_vOperatorTypes;
+
+    std::string m_sTriggerBuffer;
+    rc_trigger_t* m_pTrigger = nullptr;
 };
 
 } // namespace viewmodels
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 6acc1e28..8a4515bd 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -4,6 +4,8 @@
 
 #include "data\EmulatorContext.hh"
 
+#include "services\IConfiguration.hh"
+
 #include "ui\viewmodels\MessageBoxViewModel.hh"
 
 #include "ui\win32\bindings\GridAddressColumnBinding.hh"
@@ -14,12 +16,22 @@
 using ra::ui::viewmodels::AssetEditorViewModel;
 using ra::ui::viewmodels::AssetViewModelBase;
 using ra::ui::viewmodels::AchievementViewModel;
+using ra::ui::viewmodels::TriggerViewModel;
+using ra::ui::viewmodels::TriggerConditionViewModel;
 using ra::ui::win32::bindings::GridColumnBinding;
 
 namespace ra {
 namespace ui {
 namespace win32 {
 
+static constexpr int COLUMN_WIDTH_ID = 30;
+static constexpr int COLUMN_WIDTH_FLAG = 75;
+static constexpr int COLUMN_WIDTH_TYPE = 42;
+static constexpr int COLUMN_WIDTH_SIZE = 54;
+static constexpr int COLUMN_WIDTH_VALUE = 72;
+static constexpr int COLUMN_WIDTH_OPERATOR = 35;
+static constexpr int COLUMN_WIDTH_HITS = 84;
+
 bool AssetEditorDialog::Presenter::IsSupported(const ra::ui::WindowViewModelBase& vmViewModel) noexcept
 {
     return (dynamic_cast<const AssetEditorViewModel*>(&vmViewModel) != nullptr);
@@ -49,6 +61,136 @@ void AssetEditorDialog::Presenter::OnClosed() noexcept { m_pDialog.reset(); }
 
 // ------------------------------------
 
+class SizeColumnBinding : 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)
+    {
+    }
+
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsValueHidden(vmItems, nIndex))
+            return L"";
+
+        return GridLookupColumnBinding::GetText(vmItems, nIndex);
+    }
+
+    bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override
+    {
+        if (pProperty == *m_pTypeProperty)
+            return true;
+
+        return GridLookupColumnBinding::DependsOn(pProperty);
+    }
+
+    HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
+    {
+        auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
+        Expects(pGridBinding != nullptr);
+
+        if (IsValueHidden(pGridBinding->GetItems(), pInfo.nItemIndex))
+            return nullptr;
+
+        return GridLookupColumnBinding::CreateInPlaceEditor(hParent, pInfo);
+    }
+
+protected:
+    bool IsValueHidden(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);
+    }
+
+    const IntModelProperty* m_pTypeProperty = nullptr;
+};
+
+class ValueColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
+{
+public:
+    ValueColumnBinding(const IntModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
+        : ra::ui::win32::bindings::GridNumberColumnBinding(pBoundProperty), m_pTypeProperty(&pTypeProperty)
+    {
+    }
+
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        const auto nValue = vmItems.GetItemValue(nIndex, *m_pBoundProperty);
+
+        if (IsAddressType(vmItems, nIndex))
+            return ra::Widen(ra::ByteAddressToString(nValue));
+
+        const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+        if (pConfiguration.IsFeatureEnabled(ra::services::Feature::PreferDecimal))
+            return std::to_wstring(nValue);
+
+        return ra::StringPrintf(L"0x%02x", nValue);
+    }
+
+    bool SetText(ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex, const std::wstring& sValue) override
+    {
+        if (ra::StringStartsWith(sValue, L"0x"))
+        {
+            std::wstring sError;
+            unsigned int nValue = 0U;
+
+            if (!ParseHex(sValue, nValue, sError))
+            {
+                ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Invalid Input", sError);
+                return false;
+            }
+
+            vmItems.SetItemValue(nIndex, *m_pBoundProperty, ra::to_signed(nValue));
+            return true;
+        }
+
+        return GridNumberColumnBinding::SetText(vmItems, nIndex, sValue);
+    }
+
+    bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override
+    {
+        if (pProperty == *m_pTypeProperty)
+            return true;
+
+        return GridNumberColumnBinding::DependsOn(pProperty);
+    }
+
+protected:
+    bool IsAddressType(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);
+    }
+
+    const IntModelProperty* m_pTypeProperty = nullptr;
+};
+
+class HitsColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
+{
+public:
+    HitsColumnBinding(const IntModelProperty& pBoundProperty) noexcept
+        : ra::ui::win32::bindings::GridNumberColumnBinding(pBoundProperty)
+    {
+    }
+
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        const auto nCurrentHits = vmItems.GetItemValue(nIndex, TriggerConditionViewModel::CurrentHitsProperty);
+        const auto nRequiredHits = vmItems.GetItemValue(nIndex, *m_pBoundProperty);
+
+        return ra::StringPrintf(L"%d (%d)", nRequiredHits, nCurrentHits);
+    }
+
+    bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override
+    {
+        if (pProperty == TriggerConditionViewModel::CurrentHitsProperty)
+            return true;
+
+        return GridNumberColumnBinding::DependsOn(pProperty);
+    }
+};
+
 AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetList)
     : DialogBase(vmAssetList),
       m_bindID(vmAssetList),
@@ -57,9 +199,12 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetList)
       m_bindBadge(vmAssetList),
       m_bindPoints(vmAssetList),
       m_bindPauseOnReset(vmAssetList),
-      m_bindPauseOnTrigger(vmAssetList)
+      m_bindPauseOnTrigger(vmAssetList),
+      m_bindGroups(vmAssetList),
+      m_bindTrigger(vmAssetList)
 {
     m_bindWindow.SetInitialPosition(RelativePosition::After, RelativePosition::After, "Achievement Editor");
+    m_bindWindow.AddChildViewModel(vmAssetList.Trigger());
 
     m_bindID.BindValue(AssetEditorViewModel::IDProperty);
     m_bindName.BindText(AssetEditorViewModel::NameProperty);
@@ -70,6 +215,86 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetList)
     m_bindPauseOnReset.BindCheck(AssetEditorViewModel::PauseOnResetProperty);
     m_bindPauseOnTrigger.BindCheck(AssetEditorViewModel::PauseOnTriggerProperty);
 
+    auto pGroupColumn = std::make_unique<ra::ui::win32::bindings::GridTextColumnBinding>(
+        TriggerViewModel::GroupViewModel::LabelProperty);
+    pGroupColumn->SetHeader(L"Group");
+    pGroupColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Fill, 50);
+    m_bindGroups.BindColumn(0, std::move(pGroupColumn));
+
+    m_bindGroups.BindIsSelected(TriggerViewModel::GroupViewModel::IsSelectedProperty);
+    m_bindGroups.BindItems(vmAssetList.Trigger().Groups());
+
+    auto pIdColumn = std::make_unique<ra::ui::win32::bindings::GridNumberColumnBinding>(
+        TriggerConditionViewModel::IndexProperty);
+    pIdColumn->SetHeader(L"ID");
+    pIdColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_ID);
+    m_bindTrigger.BindColumn(0, std::move(pIdColumn));
+
+    auto pFlagColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
+        TriggerConditionViewModel::TypeProperty, vmAssetList.Trigger().ConditionTypes());
+    pFlagColumn->SetHeader(L"Flag");
+    pFlagColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_FLAG);
+    pFlagColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(1, std::move(pFlagColumn));
+
+    auto pSourceTypeColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
+        TriggerConditionViewModel::SourceTypeProperty, vmAssetList.Trigger().OperandTypes());
+    pSourceTypeColumn->SetHeader(L"Type");
+    pSourceTypeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_TYPE);
+    pSourceTypeColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(2, std::move(pSourceTypeColumn));
+
+    auto pSourceSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::SourceSizeProperty,
+        TriggerConditionViewModel::SourceTypeProperty, vmAssetList.Trigger().OperandSizes());
+    pSourceSizeColumn->SetHeader(L"Size");
+    pSourceSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
+    pSourceSizeColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(3, std::move(pSourceSizeColumn));
+
+    auto pSourceValueColumn = std::make_unique<ValueColumnBinding>(
+        TriggerConditionViewModel::SourceValueProperty, TriggerConditionViewModel::SourceTypeProperty);
+    pSourceValueColumn->SetHeader(L"Memory");
+    pSourceValueColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_VALUE);
+    pSourceValueColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(4, std::move(pSourceValueColumn));
+
+    auto pOperatorColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
+        TriggerConditionViewModel::OperatorProperty, vmAssetList.Trigger().OperatorTypes());
+    pOperatorColumn->SetHeader(L"Cmp");
+    pOperatorColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_OPERATOR);
+    pOperatorColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(5, std::move(pOperatorColumn));
+
+    auto pTargetTypeColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
+        TriggerConditionViewModel::TargetTypeProperty, vmAssetList.Trigger().OperandTypes());
+    pTargetTypeColumn->SetHeader(L"Type");
+    pTargetTypeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_TYPE);
+    pTargetTypeColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(6, std::move(pTargetTypeColumn));
+
+    auto pTargetSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::TargetSizeProperty,
+        TriggerConditionViewModel::TargetTypeProperty, vmAssetList.Trigger().OperandSizes());
+    pTargetSizeColumn->SetHeader(L"Size");
+    pTargetSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
+    pTargetSizeColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(7, std::move(pTargetSizeColumn));
+
+    auto pTargetValueColumn = std::make_unique<ValueColumnBinding>(
+        TriggerConditionViewModel::TargetValueProperty, TriggerConditionViewModel::TargetTypeProperty);
+    pTargetValueColumn->SetHeader(L"Mem/Val");
+    pTargetValueColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_VALUE);
+    pTargetValueColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(8, std::move(pTargetValueColumn));
+
+    auto pHitsColumn = std::make_unique<HitsColumnBinding>(TriggerConditionViewModel::RequiredHitsProperty);
+    pHitsColumn->SetHeader(L"Hits");
+    pHitsColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Fill, COLUMN_WIDTH_HITS);
+    pHitsColumn->SetReadOnly(false);
+    m_bindTrigger.BindColumn(9, std::move(pHitsColumn));
+
+    m_bindTrigger.BindItems(vmAssetList.Trigger().Conditions());
+
+
     using namespace ra::bitwise_ops;
     SetAnchor(IDC_RA_LISTACHIEVEMENTS, Anchor::Top | Anchor::Left | Anchor::Bottom | Anchor::Right);
 
@@ -84,6 +309,9 @@ BOOL AssetEditorDialog::OnInitDialog()
     m_bindBadge.SetControl(*this, IDC_RA_BADGENAME);
     m_bindPoints.SetControl(*this, IDC_RA_ACH_POINTS);
 
+    m_bindGroups.SetControl(*this, IDC_RA_ACH_GROUP);
+    m_bindTrigger.SetControl(*this, IDC_RA_LBX_CONDITIONS);
+
     return DialogBase::OnInitDialog();
 }
 
diff --git a/src/ui/win32/AssetEditorDialog.hh b/src/ui/win32/AssetEditorDialog.hh
index 55107f00..86d5a177 100644
--- a/src/ui/win32/AssetEditorDialog.hh
+++ b/src/ui/win32/AssetEditorDialog.hh
@@ -51,6 +51,9 @@ private:
 
     ra::ui::win32::bindings::CheckBoxBinding m_bindPauseOnReset;
     ra::ui::win32::bindings::CheckBoxBinding m_bindPauseOnTrigger;
+
+    ra::ui::win32::bindings::GridBinding m_bindGroups;
+    ra::ui::win32::bindings::GridBinding m_bindTrigger;
 };
 
 } // namespace win32
diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp
index 261fad7b..91c8f70b 100644
--- a/src/ui/win32/bindings/GridBinding.cpp
+++ b/src/ui/win32/bindings/GridBinding.cpp
@@ -512,6 +512,15 @@ void GridBinding::OnEndViewModelCollectionUpdate()
 
         SendMessage(m_hWnd, WM_SETREDRAW, TRUE, 0);
 
+        if (m_pIsSelectedProperty && !m_pUpdateSelectedItems)
+        {
+            for (gsl::index nIndex = 0; nIndex < ra::to_signed(m_vmItems->Count()); ++nIndex)
+            {
+                const bool bSelected = m_vmItems->GetItemValue(nIndex, *m_pIsSelectedProperty);
+                ListView_SetItemState(m_hWnd, nIndex, bSelected ? LVIS_SELECTED : 0, LVIS_SELECTED);
+            }
+        }
+
         if (m_bForceRepaint && m_nAdjustingScrollOffset == 0)
             ControlBinding::ForceRepaint(m_hWnd);
         else
@@ -630,10 +639,15 @@ void GridBinding::OnLvnItemChanged(const LPNMLISTVIEW pnmListView)
 
     if (m_pIsSelectedProperty)
     {
-        if (pnmListView->uNewState & LVIS_SELECTED)
-            m_vmItems->SetItemValue(nIndex, *m_pIsSelectedProperty, true);
-        else if (pnmListView->uOldState & LVIS_SELECTED)
-            m_vmItems->SetItemValue(nIndex, *m_pIsSelectedProperty, false);
+        // don't update the property on the view model while the list is being updated. assume items are
+        // being added/removed/moved and we'll reset the state correctly in OnEndViewModelCollectionUpdate.
+        if (!m_vmItems->IsUpdating())
+        {
+            if (pnmListView->uNewState & LVIS_SELECTED)
+                m_vmItems->SetItemValue(nIndex, *m_pIsSelectedProperty, true);
+            else if (pnmListView->uOldState & LVIS_SELECTED)
+                m_vmItems->SetItemValue(nIndex, *m_pIsSelectedProperty, false);
+        }
     }
 
     switch (pnmListView->uNewState & 0x3000)


Test patch:
diff --git a/tests/ui/ViewModelCollection_Tests.cpp b/tests/ui/ViewModelCollection_Tests.cpp
index 77a68226..c3079f6f 100644
--- a/tests/ui/ViewModelCollection_Tests.cpp
+++ b/tests/ui/ViewModelCollection_Tests.cpp
@@ -427,6 +427,27 @@ public:
         oNotify.AssertItemRemoved(1);
     }
 
+    TEST_METHOD(TestClearWhileUpdateSuspendedPropertyNotification)
+    {
+        ViewModelCollection<TestViewModel> vmCollection;
+        vmCollection.Add(1, L"Test1");
+        vmCollection.Add(2, L"Test2");
+
+        NotifyTargetHarness oNotify;
+        vmCollection.AddNotifyTarget(oNotify);
+        vmCollection.BeginUpdate();
+
+        vmCollection.Clear();
+        oNotify.AssertItemNotChanged(0);
+        oNotify.AssertItemNotChanged(1);
+
+        vmCollection.EndUpdate();
+        oNotify.AssertItemRemoved(0);
+        oNotify.AssertItemRemoved(1);
+
+        Assert::AreEqual({ 0U }, vmCollection.Count());
+    }
+
     TEST_METHOD(TestReplaceWhileUpdateSuspendedPropertyNotification)
     {
         ViewModelCollection<TestViewModel> vmCollection;
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index 1f717f18..e2db1cef 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -62,8 +62,6 @@ std::wstring ToString<ra::ui::viewmodels::TriggerOperandType>(
             return L"Prior";
         case ra::ui::viewmodels::TriggerOperandType::BCD:
             return L"BCD";
-        case ra::ui::viewmodels::TriggerOperandType::Inverted:
-            return L"Inverted";
         default:
             return std::to_wstring(static_cast<int>(nOperandType));
     }
@@ -171,7 +169,6 @@ public:
         ParseAndRegenerate("0xH1234=d0xH1234"); // delta
         ParseAndRegenerate("0xH1234=p0xH1234"); // prior
         ParseAndRegenerate("0xH1234=b0xH1234"); // bcd
-        ParseAndRegenerate("0xH1234=~0xH1234"); // inverted
         ParseAndRegenerate("0xH1234=1234"); // value
     }
 
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 47095ba3..65b030db 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -16,15 +16,7 @@ TEST_CLASS(TriggerViewModel_Tests)
 private:
     void Parse(TriggerViewModel& vmTrigger, const std::string& sInput)
     {
-        unsigned char pBuffer[768] = { 0 };
-        const auto nSize = rc_trigger_size(sInput.c_str());
-        Assert::IsTrue(nSize > 0 && nSize < sizeof(pBuffer));
-
-        const rc_trigger_t* pTrigger = rc_parse_trigger(pBuffer, sInput.c_str(), nullptr, 0);
-        Assert::IsNotNull(pTrigger);
-        Ensures(pTrigger != nullptr);
-
-        vmTrigger.InitializeFrom(*pTrigger);
+        vmTrigger.InitializeFrom(sInput);
     }
 
     void ParseAndRegenerate(const std::string& sInput)
@@ -66,6 +58,98 @@ public:
         Assert::IsFalse(vmTrigger.IsPauseOnTrigger());
         Assert::AreEqual({ 0U }, vmTrigger.Groups().Count());
         Assert::AreEqual(std::string(), vmTrigger.Serialize());
+
+        Assert::AreEqual({ 12U }, vmTrigger.ConditionTypes().Count());
+        Assert::AreEqual((int)TriggerConditionType::Standard, vmTrigger.ConditionTypes().GetItemAt(0)->GetId());
+        Assert::AreEqual(std::wstring(L""), vmTrigger.ConditionTypes().GetItemAt(0)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::PauseIf, vmTrigger.ConditionTypes().GetItemAt(1)->GetId());
+        Assert::AreEqual(std::wstring(L"Pause If"), vmTrigger.ConditionTypes().GetItemAt(1)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::ResetIf, vmTrigger.ConditionTypes().GetItemAt(2)->GetId());
+        Assert::AreEqual(std::wstring(L"Reset If"), vmTrigger.ConditionTypes().GetItemAt(2)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddSource, vmTrigger.ConditionTypes().GetItemAt(3)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Source"), vmTrigger.ConditionTypes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::SubSource, vmTrigger.ConditionTypes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L"Sub Source"), vmTrigger.ConditionTypes().GetItemAt(4)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddHits, vmTrigger.ConditionTypes().GetItemAt(5)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Hits"), vmTrigger.ConditionTypes().GetItemAt(5)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddAddress, vmTrigger.ConditionTypes().GetItemAt(6)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Address"), vmTrigger.ConditionTypes().GetItemAt(6)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AndNext, vmTrigger.ConditionTypes().GetItemAt(7)->GetId());
+        Assert::AreEqual(std::wstring(L"And Next"), vmTrigger.ConditionTypes().GetItemAt(7)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::OrNext, vmTrigger.ConditionTypes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"Or Next"), vmTrigger.ConditionTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::Measured, vmTrigger.ConditionTypes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"Measured"), vmTrigger.ConditionTypes().GetItemAt(9)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::MeasuredIf, vmTrigger.ConditionTypes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"Measured If"), vmTrigger.ConditionTypes().GetItemAt(10)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::Trigger, vmTrigger.ConditionTypes().GetItemAt(11)->GetId());
+        Assert::AreEqual(std::wstring(L"Trigger"), vmTrigger.ConditionTypes().GetItemAt(11)->GetLabel());
+
+        Assert::AreEqual({ 5U }, vmTrigger.OperandTypes().Count());
+        Assert::AreEqual((int)TriggerOperandType::Address, vmTrigger.OperandTypes().GetItemAt(0)->GetId());
+        Assert::AreEqual(std::wstring(L"Mem"), vmTrigger.OperandTypes().GetItemAt(0)->GetLabel());
+        Assert::AreEqual((int)TriggerOperandType::Value, vmTrigger.OperandTypes().GetItemAt(1)->GetId());
+        Assert::AreEqual(std::wstring(L"Value"), vmTrigger.OperandTypes().GetItemAt(1)->GetLabel());
+        Assert::AreEqual((int)TriggerOperandType::Delta, vmTrigger.OperandTypes().GetItemAt(2)->GetId());
+        Assert::AreEqual(std::wstring(L"Delta"), vmTrigger.OperandTypes().GetItemAt(2)->GetLabel());
+        Assert::AreEqual((int)TriggerOperandType::Prior, vmTrigger.OperandTypes().GetItemAt(3)->GetId());
+        Assert::AreEqual(std::wstring(L"Prior"), vmTrigger.OperandTypes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)TriggerOperandType::BCD, vmTrigger.OperandTypes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L"BCD"), vmTrigger.OperandTypes().GetItemAt(4)->GetLabel());
+
+        Assert::AreEqual({ 15U }, vmTrigger.OperandSizes().Count());
+        Assert::AreEqual((int)MemSize::Bit_0, vmTrigger.OperandSizes().GetItemAt(0)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit0"), vmTrigger.OperandSizes().GetItemAt(0)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_1, vmTrigger.OperandSizes().GetItemAt(1)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit1"), vmTrigger.OperandSizes().GetItemAt(1)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_2, vmTrigger.OperandSizes().GetItemAt(2)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit2"), vmTrigger.OperandSizes().GetItemAt(2)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_3, vmTrigger.OperandSizes().GetItemAt(3)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit3"), vmTrigger.OperandSizes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_4, vmTrigger.OperandSizes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit4"), vmTrigger.OperandSizes().GetItemAt(4)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_5, vmTrigger.OperandSizes().GetItemAt(5)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit5"), vmTrigger.OperandSizes().GetItemAt(5)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_6, vmTrigger.OperandSizes().GetItemAt(6)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit6"), vmTrigger.OperandSizes().GetItemAt(6)->GetLabel());
+        Assert::AreEqual((int)MemSize::Bit_7, vmTrigger.OperandSizes().GetItemAt(7)->GetId());
+        Assert::AreEqual(std::wstring(L"Bit7"), vmTrigger.OperandSizes().GetItemAt(7)->GetLabel());
+        Assert::AreEqual((int)MemSize::Nibble_Lower, vmTrigger.OperandSizes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"Lower4"), vmTrigger.OperandSizes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)MemSize::Nibble_Upper, vmTrigger.OperandSizes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"Upper4"), vmTrigger.OperandSizes().GetItemAt(9)->GetLabel());
+        Assert::AreEqual((int)MemSize::EightBit, vmTrigger.OperandSizes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"8-bit"), vmTrigger.OperandSizes().GetItemAt(10)->GetLabel());
+        Assert::AreEqual((int)MemSize::SixteenBit, vmTrigger.OperandSizes().GetItemAt(11)->GetId());
+        Assert::AreEqual(std::wstring(L"16-bit"), vmTrigger.OperandSizes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)MemSize::TwentyFourBit, vmTrigger.OperandSizes().GetItemAt(12)->GetId());
+        Assert::AreEqual(std::wstring(L"24-bit"), vmTrigger.OperandSizes().GetItemAt(12)->GetLabel());
+        Assert::AreEqual((int)MemSize::ThirtyTwoBit, vmTrigger.OperandSizes().GetItemAt(13)->GetId());
+        Assert::AreEqual(std::wstring(L"32-bit"), vmTrigger.OperandSizes().GetItemAt(13)->GetLabel());
+        Assert::AreEqual((int)MemSize::BitCount, vmTrigger.OperandSizes().GetItemAt(14)->GetId());
+        Assert::AreEqual(std::wstring(L"BitCount"), vmTrigger.OperandSizes().GetItemAt(14)->GetLabel());
+
+        Assert::AreEqual({ 10U }, vmTrigger.OperatorTypes().Count());
+        Assert::AreEqual((int)TriggerOperatorType::Equals, vmTrigger.OperatorTypes().GetItemAt(0)->GetId());
+        Assert::AreEqual(std::wstring(L"="), vmTrigger.OperatorTypes().GetItemAt(0)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::LessThan, vmTrigger.OperatorTypes().GetItemAt(1)->GetId());
+        Assert::AreEqual(std::wstring(L"<"), vmTrigger.OperatorTypes().GetItemAt(1)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::LessThanOrEqual, vmTrigger.OperatorTypes().GetItemAt(2)->GetId());
+        Assert::AreEqual(std::wstring(L"<="), vmTrigger.OperatorTypes().GetItemAt(2)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::GreaterThan, vmTrigger.OperatorTypes().GetItemAt(3)->GetId());
+        Assert::AreEqual(std::wstring(L">"), vmTrigger.OperatorTypes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::GreaterThanOrEqual, vmTrigger.OperatorTypes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L">="), vmTrigger.OperatorTypes().GetItemAt(4)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::NotEquals, vmTrigger.OperatorTypes().GetItemAt(5)->GetId());
+        Assert::AreEqual(std::wstring(L"!="), vmTrigger.OperatorTypes().GetItemAt(5)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::None, vmTrigger.OperatorTypes().GetItemAt(6)->GetId());
+        Assert::AreEqual(std::wstring(L""), vmTrigger.OperatorTypes().GetItemAt(6)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::Multiply, vmTrigger.OperatorTypes().GetItemAt(7)->GetId());
+        Assert::AreEqual(std::wstring(L"*"), vmTrigger.OperatorTypes().GetItemAt(7)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::Divide, vmTrigger.OperatorTypes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"/"), vmTrigger.OperatorTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::BitwiseAnd, vmTrigger.OperatorTypes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"&"), vmTrigger.OperatorTypes().GetItemAt(9)->GetLabel());
     }
 
     TEST_METHOD(TestParseAndRegenerateCoreOnly)
@@ -89,10 +173,12 @@ public:
         Parse(vmTrigger, "0xH1234=0xH2345S0xX5555=1");
 
         TriggerMonitor pMonitor(vmTrigger);
-        vmTrigger.Groups().GetItemAt(0)->Conditions().GetItemAt(0)->SetOperator(ra::ui::viewmodels::TriggerOperatorType::LessThan);
+        vmTrigger.SetSelectedGroupIndex(0);
+        vmTrigger.Conditions().GetItemAt(0)->SetOperator(ra::ui::viewmodels::TriggerOperatorType::LessThan);
         Assert::AreEqual(std::string("0xH1234<0xH2345S0xX5555=1"), pMonitor.sNewTrigger);
 
-        vmTrigger.Groups().GetItemAt(1)->Conditions().GetItemAt(0)->SetTargetValue(9U);
+        vmTrigger.SetSelectedGroupIndex(1);
+        vmTrigger.Conditions().GetItemAt(0)->SetTargetValue(9U);
         Assert::AreEqual(std::string("0xH1234<0xH2345S0xX5555=9"), pMonitor.sNewTrigger);
     }
 
@@ -102,16 +188,18 @@ public:
         Parse(vmTrigger, "0xH1234=0xH2345S0xX5555=1");
 
         TriggerMonitor pMonitor(vmTrigger);
-        vmTrigger.Groups().GetItemAt(0)->Conditions().Add();
+        vmTrigger.SetSelectedGroupIndex(0);
+        vmTrigger.Conditions().Add();
         Assert::AreEqual(std::string("0xH1234=0xH2345_0xH0000=0S0xX5555=1"), pMonitor.sNewTrigger);
 
-        vmTrigger.Groups().GetItemAt(0)->Conditions().RemoveAt(0);
+        vmTrigger.Conditions().RemoveAt(0);
         Assert::AreEqual(std::string("0xH0000=0S0xX5555=1"), pMonitor.sNewTrigger);
 
-        vmTrigger.Groups().GetItemAt(1)->Conditions().Add();
+        vmTrigger.SetSelectedGroupIndex(1);
+        vmTrigger.Conditions().Add();
         Assert::AreEqual(std::string("0xH0000=0S0xX5555=1_0xH0000=0"), pMonitor.sNewTrigger);
 
-        vmTrigger.Groups().GetItemAt(1)->Conditions().RemoveAt(0);
+        vmTrigger.Conditions().RemoveAt(0);
         Assert::AreEqual(std::string("0xH0000=0S0xH0000=0"), pMonitor.sNewTrigger);
     }
 
@@ -121,10 +209,11 @@ public:
         Parse(vmTrigger, "0xH1234=0xH2345S0xX5555=1");
 
         TriggerMonitor pMonitor(vmTrigger);
-        vmTrigger.Groups().GetItemAt(0)->Conditions().Add();
+        vmTrigger.SetSelectedGroupIndex(0);
+        vmTrigger.Conditions().Add();
         Assert::AreEqual(std::string("0xH1234=0xH2345_0xH0000=0S0xX5555=1"), pMonitor.sNewTrigger);
 
-        vmTrigger.Groups().GetItemAt(0)->Conditions().GetItemAt(1)->SetSourceValue(1234U);
+        vmTrigger.Conditions().GetItemAt(1)->SetSourceValue(1234U);
         Assert::AreEqual(std::string("0xH1234=0xH2345_0xH04d2=0S0xX5555=1"), pMonitor.sNewTrigger);
     }
 };