Repository: RetroAchievements/RAIntegration
Pull request #619 (2020-10-24 19:07:52)

hook up new achievement button


Patch:
diff --git a/src/data/GameContext.cpp b/src/data/GameContext.cpp
index 4847e3b7..5721a34e 100644
--- a/src/data/GameContext.cpp
+++ b/src/data/GameContext.cpp
@@ -150,7 +150,8 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
         if (nCategory != Achievement::Category::Core && nCategory != Achievement::Category::Unofficial)
             continue;
 
-        auto& pAchievement = NewAchievement(nCategory);
+        auto& pAchievement = *m_vAchievements.emplace_back(std::make_unique<Achievement>());
+        pAchievement.SetCategory(nCategory);
         pAchievement.SetID(pAchievementData.Id);
         CopyAchievementData(pAchievement, pAchievementData);
 
@@ -657,6 +658,17 @@ Achievement& GameContext::NewAchievement(Achievement::Category nType)
     Achievement& pAchievement = *m_vAchievements.emplace_back(std::make_unique<Achievement>());
     pAchievement.SetCategory(nType);
     pAchievement.SetID(m_nNextLocalId++);
+
+    auto vmAchievement = std::make_unique<ra::ui::viewmodels::AchievementViewModel>();
+    vmAchievement->SetID(pAchievement.ID());
+    vmAchievement->SetCategory(ra::itoe<ra::ui::viewmodels::AssetCategory>(ra::etoi(nType)));
+    vmAchievement->SetPoints(0);
+    vmAchievement->CreateServerCheckpoint();
+    vmAchievement->CreateLocalCheckpoint();
+
+    auto& vmAssets = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetList;
+    vmAssets.Assets().Append(std::move(vmAchievement));
+
     return pAchievement;
 }
 
diff --git a/src/data/GameContext.hh b/src/data/GameContext.hh
index e27929d1..2b0e6ce0 100644
--- a/src/data/GameContext.hh
+++ b/src/data/GameContext.hh
@@ -331,7 +331,7 @@ protected:
     std::string m_sServerRichPresenceMD5;
     bool m_bRichPresenceFromFile = false;
 
-    ra::AchievementID m_nNextLocalId = 0;
+    ra::AchievementID m_nNextLocalId = FirstLocalId;
     static const ra::AchievementID FirstLocalId = 111000001;
 
     std::vector<std::unique_ptr<Achievement>> m_vAchievements;
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 854d03b5..7089597e 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -6,6 +6,7 @@
 #include "services\IThreadPool.hh"
 #include "services\ServiceLocator.hh"
 
+#include "ui\viewmodels\MessageBoxViewModel.hh"
 #include "ui\viewmodels\WindowManager.hh"
 
 #include "Exports.hh"
@@ -24,12 +25,15 @@ const StringModelProperty AssetListViewModel::SaveButtonTextProperty("AssetListV
 const StringModelProperty AssetListViewModel::PublishButtonTextProperty("AssetListViewModel", "SaveButtonText", L"&Publish All");
 const StringModelProperty AssetListViewModel::RefreshButtonTextProperty("AssetListViewModel", "SaveButtonText", L"&Refresh All");
 const StringModelProperty AssetListViewModel::RevertButtonTextProperty("AssetListViewModel", "SaveButtonText", L"Re&vert All");
+const BoolModelProperty AssetListViewModel::CanActivateProperty("AssetListViewModel", "CanActivate", false);
 const BoolModelProperty AssetListViewModel::CanSaveProperty("AssetListViewModel", "CanSave", false);
 const BoolModelProperty AssetListViewModel::CanPublishProperty("AssetListViewModel", "CanPublish", false);
 const BoolModelProperty AssetListViewModel::CanRefreshProperty("AssetListViewModel", "CanRefresh", false);
 const BoolModelProperty AssetListViewModel::CanRevertProperty("AssetListViewModel", "CanRevert", false);
+const BoolModelProperty AssetListViewModel::CanCreateProperty("AssetListViewModel", "CanCreate", false);
 const BoolModelProperty AssetListViewModel::CanCloneProperty("AssetListViewModel", "CanClone", false);
 const IntModelProperty AssetListViewModel::FilterCategoryProperty("AssetListViewModel", "FilterCategory", ra::etoi(AssetCategory::Core));
+const IntModelProperty AssetListViewModel::EnsureVisibleAssetIndexProperty("AssetListViewModel", "EnsureVisibleAssetIndex", -1);
 
 AssetListViewModel::AssetListViewModel() noexcept
 {
@@ -202,6 +206,10 @@ void AssetListViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args
 {
     if (args.Property == FilterCategoryProperty)
         ApplyFilter();
+    else if (args.Property == GameIdProperty)
+        UpdateButtons();
+
+    WindowViewModelBase::OnValueChanged(args);
 }
 
 void AssetListViewModel::ApplyFilter()
@@ -346,7 +354,26 @@ bool AssetListViewModel::HasSelection(AssetType nAssetType) const
 void AssetListViewModel::FilteredListMonitor::OnViewModelBoolValueChanged(_UNUSED gsl::index nIndex, const BoolModelProperty::ChangeArgs& args)
 {
     if (args.Property == AssetSummaryViewModel::IsSelectedProperty)
+    {
+        if (m_pOwner->FilteredAssets().IsUpdating())
+            m_bUpdateButtonsPending = true;
+        else
+            m_pOwner->UpdateButtons();
+    }
+}
+
+void AssetListViewModel::FilteredListMonitor::OnBeginViewModelCollectionUpdate() noexcept
+{
+    m_bUpdateButtonsPending = false;
+}
+
+void AssetListViewModel::FilteredListMonitor::OnEndViewModelCollectionUpdate()
+{
+    if (m_bUpdateButtonsPending)
+    {
+        m_bUpdateButtonsPending = false;
         m_pOwner->UpdateButtons();
+    }
 }
 
 void AssetListViewModel::UpdateButtons()
@@ -376,47 +403,58 @@ void AssetListViewModel::DoUpdateButtons()
     bool bHasModified = false;
     bool bHasUnpublished = false;
 
-    for (size_t i = 0; i < m_vFilteredAssets.Count(); ++i)
+    if (GetGameId() == 0)
     {
-        const auto* pItem = m_vFilteredAssets.GetItemAt(i);
-        if (pItem != nullptr && pItem->IsSelected())
+        SetValue(CanCreateProperty, false);
+        SetValue(CanActivateProperty, false);
+    }
+    else
+    {
+        SetValue(CanCreateProperty, true);
+        SetValue(CanActivateProperty, true);
+
+        for (size_t i = 0; i < m_vFilteredAssets.Count(); ++i)
         {
-            switch (pItem->GetCategory())
+            const auto* pItem = m_vFilteredAssets.GetItemAt(i);
+            if (pItem != nullptr && pItem->IsSelected())
             {
-                case AssetCategory::Core:
-                    bHasCoreSelection = true;
-                    break;
-                case AssetCategory::Unofficial:
-                    bHasUnofficialSelection = true;
-                    break;
-                case AssetCategory::Local:
-                    bHasLocalSelection = true;
-                    break;
-                default:
-                    break;
-            }
+                switch (pItem->GetCategory())
+                {
+                    case AssetCategory::Core:
+                        bHasCoreSelection = true;
+                        break;
+                    case AssetCategory::Unofficial:
+                        bHasUnofficialSelection = true;
+                        break;
+                    case AssetCategory::Local:
+                        bHasLocalSelection = true;
+                        break;
+                    default:
+                        break;
+                }
 
-            switch (pItem->GetState())
-            {
-                case AssetState::Inactive:
-                case AssetState::Triggered:
-                    bHasInactiveSelection = true;
-                    break;
-                default:
-                    bHasActiveSelection = true;
-                    break;
-            }
+                switch (pItem->GetState())
+                {
+                    case AssetState::Inactive:
+                    case AssetState::Triggered:
+                        bHasInactiveSelection = true;
+                        break;
+                    default:
+                        bHasActiveSelection = true;
+                        break;
+                }
 
-            switch (pItem->GetChanges())
-            {
-                case AssetChanges::Modified:
-                    bHasModifiedSelection = true;
-                    break;
-                case AssetChanges::Unpublished:
-                    bHasUnpublishedSelection = true;
-                    break;
-                default:
-                    break;
+                switch (pItem->GetChanges())
+                {
+                    case AssetChanges::Modified:
+                        bHasModifiedSelection = true;
+                        break;
+                    case AssetChanges::Unpublished:
+                        bHasUnpublishedSelection = true;
+                        break;
+                    default:
+                        break;
+                }
             }
         }
     }
@@ -654,9 +692,62 @@ void AssetListViewModel::RevertSelected()
         return;
 }
 
-void AssetListViewModel::CreateNew() noexcept
+void AssetListViewModel::CreateNew()
 {
+    FilteredAssets().BeginUpdate();
+
+    SetFilterCategory(AssetCategory::Local);
+
+    // this creates the viewmodel too
+    auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::GameContext>();
+    const auto& pAchievement = pGameContext.NewAchievement(Achievement::Category::Local);
+
+#ifdef RA_UTEST
+    {
+        // in unit tests, copy the view model from the MockWindowManager to the harness
+        auto& pWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
+        const auto* vmNewAchievement = dynamic_cast<AchievementViewModel*>(pWindowManager.AssetList.Assets().GetItemAt(pWindowManager.AssetList.Assets().Count() - 1));
+        Expects(vmNewAchievement != nullptr);
+
+        auto vmAchievement = std::make_unique<ra::ui::viewmodels::AchievementViewModel>();
+        vmAchievement->SetID(vmNewAchievement->GetID());
+        vmAchievement->SetCategory(vmNewAchievement->GetCategory());
+        vmAchievement->SetPoints(vmNewAchievement->GetPoints());
+        vmAchievement->CreateServerCheckpoint();
+        vmAchievement->CreateLocalCheckpoint();
+
+        Assets().Append(std::move(vmAchievement));
+    }
+#endif
+
+    // select the new viewmodel, and deselect everything else
+    gsl::index nIndex = -1;
+    for (gsl::index i = 0; i < ra::to_signed(m_vFilteredAssets.Count()); ++i)
+    {
+        auto* pItem = m_vFilteredAssets.GetItemAt(i);
+        if (pItem != nullptr)
+        {
+            if (pItem->GetId() == ra::to_signed(pAchievement.ID()) && pItem->GetType() == AssetType::Achievement)
+            {
+                pItem->SetSelected(true);
+                nIndex = i;
+            }
+            else
+            {
+                pItem->SetSelected(false);
+            }
+        }
+    }
+
+    FilteredAssets().EndUpdate();
+
+    // assert: only Core achievements are tallied, so we shouldn't need to call UpdateTotals
+
+    SetValue(EnsureVisibleAssetIndexProperty, gsl::narrow_cast<int>(nIndex));
 
+    auto* vmAsset = m_vFilteredAssets.GetItemAt(nIndex);
+    if (vmAsset)
+        OpenEditor(vmAsset);
 }
 
 void AssetListViewModel::CloneSelected()
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index b7ae8419..8e0a9f5c 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -104,6 +104,8 @@ public:
 
     static const StringModelProperty ActivateButtonTextProperty;
     const std::wstring& GetActivateButtonText() const { return GetValue(ActivateButtonTextProperty); }
+    static const BoolModelProperty CanActivateProperty;
+    bool CanActivate() const { return GetValue(CanActivateProperty); }
     void ActivateSelected();
 
     static const StringModelProperty SaveButtonTextProperty;
@@ -130,7 +132,9 @@ public:
     bool CanRevert() const { return GetValue(CanRevertProperty); }
     void RevertSelected();
 
-    void CreateNew() noexcept;
+    static const BoolModelProperty CanCreateProperty;
+    bool CanCreate() const { return GetValue(CanCreateProperty); }
+    void CreateNew();
 
     static const BoolModelProperty CanCloneProperty;
     bool CanClone() const { return GetValue(CanCloneProperty); }
@@ -189,6 +193,11 @@ public:
 
     void OpenEditor(const AssetSummaryViewModel* pAsset);
 
+    /// <summary>
+    /// The <see cref="ModelProperty" /> for the index of the asset that should be made visible.
+    /// </summary>
+    static const IntModelProperty EnsureVisibleAssetIndexProperty;
+
 private:
     // ViewModelCollectionBase::NotifyTarget
     void OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args) override;
@@ -204,8 +213,11 @@ private:
     {
     public:
         AssetListViewModel *m_pOwner = nullptr;
+        bool m_bUpdateButtonsPending = false;
 
         void OnViewModelBoolValueChanged(gsl::index nIndex, const BoolModelProperty::ChangeArgs& args) override;
+        void OnBeginViewModelCollectionUpdate() noexcept override;
+        void OnEndViewModelCollectionUpdate() override;
     };
     FilteredListMonitor m_pFilteredListMonitor;
 
diff --git a/src/ui/win32/AssetListDialog.cpp b/src/ui/win32/AssetListDialog.cpp
index cb470016..faad322f 100644
--- a/src/ui/win32/AssetListDialog.cpp
+++ b/src/ui/win32/AssetListDialog.cpp
@@ -97,6 +97,7 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
         if (pSummary)
             vmAssets->OpenEditor(pSummary);
     });
+    m_bindAssets.BindEnsureVisible(AssetListViewModel::EnsureVisibleAssetIndexProperty);
     m_bindAssets.BindItems(vmAssetList.FilteredAssets());
 
     m_bindCoreAchievements.BindCheck(AssetListViewModel::FilterCategoryProperty, ra::etoi(ra::ui::viewmodels::AssetCategory::Core));
@@ -107,7 +108,9 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     m_bindWindow.BindLabel(IDC_RA_NUMACH, AssetListViewModel::AchievementCountProperty);
     m_bindWindow.BindLabel(IDC_RA_POINT_TOTAL, AssetListViewModel::TotalPointsProperty);
 
+    m_bindWindow.BindEnabled(IDC_RA_RESET_ACH, AssetListViewModel::CanActivateProperty);
     m_bindWindow.BindLabel(IDC_RA_RESET_ACH, AssetListViewModel::ActivateButtonTextProperty);
+    m_bindWindow.BindEnabled(IDC_RA_ADD_ACH, AssetListViewModel::CanCreateProperty);
     m_bindWindow.BindLabel(IDC_RA_COMMIT_ACH, AssetListViewModel::SaveButtonTextProperty);
     m_bindWindow.BindEnabled(IDC_RA_COMMIT_ACH, AssetListViewModel::CanSaveProperty);
     m_bindWindow.BindLabel(IDC_RA_PROMOTE_ACH, AssetListViewModel::PublishButtonTextProperty);
@@ -116,6 +119,7 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     m_bindWindow.BindEnabled(IDC_RA_DOWNLOAD_ACH, AssetListViewModel::CanRefreshProperty);
     m_bindWindow.BindLabel(IDC_RA_REVERTSELECTED, AssetListViewModel::RevertButtonTextProperty);
     m_bindWindow.BindEnabled(IDC_RA_REVERTSELECTED, AssetListViewModel::CanRevertProperty);
+    m_bindWindow.BindEnabled(IDC_RA_ADD_ACH, AssetListViewModel::CanCreateProperty);
     m_bindWindow.BindEnabled(IDC_RA_CLONE_ACH, AssetListViewModel::CanCloneProperty);
 
     m_bindProcessingActive.BindCheck(AssetListViewModel::IsProcessingActiveProperty);
diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp
index 368c5354..a7e56b86 100644
--- a/src/ui/win32/bindings/GridBinding.cpp
+++ b/src/ui/win32/bindings/GridBinding.cpp
@@ -275,6 +275,14 @@ void GridBinding::OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs&
             return;
         }
     }
+    else if (m_pEnsureVisibleProperty && *m_pEnsureVisibleProperty == args.Property)
+    {
+        if (args.tNewValue >= 0)
+        {
+            EnsureVisible(gsl::narrow_cast<gsl::index>(args.tNewValue));
+            SetValue(*m_pEnsureVisibleProperty, -1);
+        }
+    }
 }
 
 void GridBinding::OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args)
@@ -362,6 +370,12 @@ void GridBinding::OnViewModelStringValueChanged(gsl::index nIndex, const StringM
     }
 }
 
+void GridBinding::EnsureVisible(gsl::index nIndex)
+{
+    if (nIndex >= 0 && nIndex < gsl::narrow_cast<gsl::index>(m_vmItems->Count()))
+        ListView_EnsureVisible(m_hWnd, gsl::narrow_cast<int>(nIndex), FALSE);
+}
+
 void GridBinding::DeselectAll() noexcept
 {
     int nScan = - 1;
@@ -508,10 +522,11 @@ void GridBinding::OnEndViewModelCollectionUpdate()
 {
     if (m_hWnd)
     {
-        CheckForScrollBar();
-
+        // enable redraw before calling CheckForScrollBar to ensure metrics are updated
         SendMessage(m_hWnd, WM_SETREDRAW, TRUE, 0);
 
+        CheckForScrollBar();
+
         if (m_pIsSelectedProperty && !m_pUpdateSelectedItems)
         {
             for (gsl::index nIndex = 0; nIndex < ra::to_signed(m_vmItems->Count()); ++nIndex)
@@ -544,6 +559,11 @@ void GridBinding::BindIsSelected(const BoolModelProperty& pIsSelectedProperty) n
     m_pIsSelectedProperty = &pIsSelectedProperty;
 }
 
+void GridBinding::BindEnsureVisible(const IntModelProperty& pEnsureVisibleProperty) noexcept
+{
+    m_pEnsureVisibleProperty = &pEnsureVisibleProperty;
+}
+
 void GridBinding::BindRowColor(const IntModelProperty& pRowColorProperty) noexcept
 {
     m_pRowColorProperty = &pRowColorProperty;
diff --git a/src/ui/win32/bindings/GridBinding.hh b/src/ui/win32/bindings/GridBinding.hh
index 36b7d8bd..6718a535 100644
--- a/src/ui/win32/bindings/GridBinding.hh
+++ b/src/ui/win32/bindings/GridBinding.hh
@@ -34,6 +34,7 @@ public:
         std::function<void(gsl::index, gsl::index, bool)> pUpdateSelectedItems);
 
     void BindIsSelected(const BoolModelProperty& pIsSelectedProperty) noexcept;
+    void BindEnsureVisible(const IntModelProperty& pEnsureVisibleProperty) noexcept;
     void BindRowColor(const IntModelProperty& pRowColorProperty) noexcept;
 
     void SetDoubleClickHandler(std::function<void(gsl::index)> pHandler);
@@ -63,6 +64,8 @@ public:
     void DeselectAll() noexcept;
     int UpdateSelected(const IntModelProperty& pProperty, int nNewValue);
 
+    virtual void EnsureVisible(gsl::index nIndex) noexcept(false);
+
 protected:
     void UpdateLayout();
     virtual void UpdateAllItems();
@@ -88,6 +91,7 @@ protected:
     std::vector<int> m_vColumnWidths;
     ViewModelCollectionBase* m_vmItems = nullptr;
     const BoolModelProperty* m_pIsSelectedProperty = nullptr;
+    const IntModelProperty* m_pEnsureVisibleProperty = nullptr;
 
     int m_nScrollOffset = 0;
 
diff --git a/src/ui/win32/bindings/MultiLineGridBinding.cpp b/src/ui/win32/bindings/MultiLineGridBinding.cpp
index 1669ba30..b3ab5d82 100644
--- a/src/ui/win32/bindings/MultiLineGridBinding.cpp
+++ b/src/ui/win32/bindings/MultiLineGridBinding.cpp
@@ -392,7 +392,7 @@ void MultiLineGridBinding::OnNmDblClick(const NMITEMACTIVATE* pnmItemActivate)
 
 void MultiLineGridBinding::EnsureVisible(gsl::index nIndex)
 {
-    if (nIndex < gsl::narrow_cast<gsl::index>(m_vItemMetrics.size()))
+    if (nIndex >= 0 && nIndex < gsl::narrow_cast<gsl::index>(m_vItemMetrics.size()))
     {
         const auto nLine = m_vItemMetrics.at(nIndex).nFirstLine;
         ListView_EnsureVisible(m_hWnd, nLine, FALSE);
diff --git a/src/ui/win32/bindings/MultiLineGridBinding.hh b/src/ui/win32/bindings/MultiLineGridBinding.hh
index d7c3f2a0..dd1f67c1 100644
--- a/src/ui/win32/bindings/MultiLineGridBinding.hh
+++ b/src/ui/win32/bindings/MultiLineGridBinding.hh
@@ -26,7 +26,7 @@ public:
     void OnNmClick(const NMITEMACTIVATE* pnmItemActivate) override;
     void OnNmDblClick(const NMITEMACTIVATE* pnmItemActivate) override;
 
-    void EnsureVisible(gsl::index nIndex);
+    void EnsureVisible(gsl::index nIndex) override;
 
 protected:
     void UpdateAllItems() override;


Test patch:
diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp
index 6101caad..31cee3fe 100644
--- a/tests/Exports_Tests.cpp
+++ b/tests/Exports_Tests.cpp
@@ -384,6 +384,7 @@ private:
         MockEmulatorContext mockEmulatorContext;
         MockSurfaceFactory mockSurfaceFactory;
         MockOverlayTheme mockTheme;
+        MockWindowManager mockWindowManager;
 
         DoAchievementsFrameHarness() noexcept
         {
diff --git a/tests/services/AchievementRuntime_Tests.cpp b/tests/services/AchievementRuntime_Tests.cpp
index 7d54efbf..c883b411 100644
--- a/tests/services/AchievementRuntime_Tests.cpp
+++ b/tests/services/AchievementRuntime_Tests.cpp
@@ -4,7 +4,9 @@
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockFileSystem.hh"
 #include "tests\mocks\MockGameContext.hh"
+#include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
+#include "tests\mocks\MockWindowManager.hh"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
@@ -49,6 +51,8 @@ public:
     ra::data::mocks::MockGameContext mockGameContext;
     ra::data::mocks::MockUserContext mockUserContext;
     ra::services::mocks::MockFileSystem mockFileSystem;
+    ra::services::mocks::MockThreadPool mockThreadPool;
+    ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
     rc_memref_value_t* GetMemRefs() noexcept
     {
diff --git a/tests/services/GameIdentifier_Tests.cpp b/tests/services/GameIdentifier_Tests.cpp
index 135409cf..5e0eb030 100644
--- a/tests/services/GameIdentifier_Tests.cpp
+++ b/tests/services/GameIdentifier_Tests.cpp
@@ -16,6 +16,7 @@
 #include "tests\mocks\MockSessionTracker.hh"
 #include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
+#include "tests\mocks\MockWindowManager.hh"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
@@ -75,6 +76,7 @@ public:
     ra::services::mocks::MockConfiguration mockConfiguration;
     ra::ui::mocks::MockDesktop mockDesktop;
     ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager;
+    ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
 private:
     ra::services::mocks::MockAudioSystem mockAudioSystem;
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index 0d93c017..49173647 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -7,9 +7,11 @@
 #include "tests\RA_UnitTestHelpers.h"
 
 #include "tests\mocks\MockAchievementRuntime.hh"
+#include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockThreadPool.hh"
+#include "tests\mocks\MockWindowManager.hh"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
@@ -28,6 +30,8 @@ private:
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::data::mocks::MockGameContext mockGameContext;
+        ra::ui::mocks::MockDesktop mockDesktop;
+        ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
         void AddAchievement(AssetCategory nCategory, unsigned nPoints, const std::wstring& sTitle)
         {
@@ -478,15 +482,57 @@ public:
         Assert::AreEqual(ra::ui::viewmodels::AssetState::Active, pItem->GetState());
     }
 
+    TEST_METHOD(TestUpdateButtonsNoGame)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetFilterCategory(AssetCategory::Core);
+
+        Assert::AreEqual({ 0U }, vmAssetList.FilteredAssets().Count());
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsFalse(vmAssetList.CanActivate());
+        Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
+        Assert::IsFalse(vmAssetList.CanSave());
+        Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
+        Assert::IsFalse(vmAssetList.CanPublish());
+        Assert::AreEqual(std::wstring(L"&Refresh All"), vmAssetList.GetRefreshButtonText());
+        Assert::IsFalse(vmAssetList.CanRefresh());
+        Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
+        Assert::IsFalse(vmAssetList.CanRevert());
+        Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsFalse(vmAssetList.CanCreate());
+
+        vmAssetList.SetGameId(1U);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.SetGameId(0U);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsFalse(vmAssetList.CanActivate());
+        Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
+        Assert::IsFalse(vmAssetList.CanSave());
+        Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
+        Assert::IsFalse(vmAssetList.CanPublish());
+        Assert::AreEqual(std::wstring(L"&Refresh All"), vmAssetList.GetRefreshButtonText());
+        Assert::IsFalse(vmAssetList.CanRefresh());
+        Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
+        Assert::IsFalse(vmAssetList.CanRevert());
+        Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsFalse(vmAssetList.CanCreate());
+    }
+
     TEST_METHOD(TestUpdateButtonsNoAssets)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
 
         Assert::AreEqual({ 0U }, vmAssetList.FilteredAssets().Count());
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -496,11 +542,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsFalse(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsNoSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
 
@@ -510,6 +558,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -519,11 +568,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsInactiveSingleSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
 
@@ -535,6 +586,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -544,11 +596,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsActiveSingleSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -561,6 +615,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -570,11 +625,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsActiveAndInactiveSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -589,6 +646,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -598,11 +656,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsChangeSelectionUpdatesButtons)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -615,6 +675,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -624,11 +685,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.FilteredAssets().GetItemAt(1)->SetSelected(true); // active
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -638,11 +701,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true); // inactive (both selected)
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -652,11 +717,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.FilteredAssets().GetItemAt(1)->SetSelected(false); // active (only inactive selected)
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -666,11 +733,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(false); // inactive (nothing selected)
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -680,11 +749,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsRemoveItems)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -698,6 +769,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -707,12 +779,14 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().RemoveAt(2); // remove selected item (which was active), nothing selected
         Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -722,12 +796,14 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().RemoveAt(0); // remove unselected item (which was inactive), nothing remaining
         Assert::AreEqual({ 0U }, vmAssetList.FilteredAssets().Count());
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -737,12 +813,14 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert()); // still has non-visible items
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Ach1"); // add back one achievement (inactive, unselected)
         Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -752,11 +830,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsChangeFilter)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -769,6 +849,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -778,6 +859,7 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.SetFilterCategory(AssetCategory::Unofficial);
         Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
@@ -785,6 +867,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate All"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -794,11 +877,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert All"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsFalse(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsModifiedSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Modified");       
@@ -811,6 +896,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save"), vmAssetList.GetSaveButtonText());
         Assert::IsTrue(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -820,11 +906,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsModifiedUnselected)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(0)->SetName(L"Modified");
@@ -837,6 +925,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsTrue(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -846,11 +935,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsModifiedChanges)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
 
@@ -861,6 +952,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -870,11 +962,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Modified");
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save"), vmAssetList.GetSaveButtonText());
         Assert::IsTrue(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -884,11 +978,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Ach3");
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -898,11 +994,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsUnpublishedSelection)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Modified");
@@ -916,6 +1014,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish"), vmAssetList.GetPublishButtonText());
@@ -925,11 +1024,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsUnpublishedUnselected)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(0)->SetName(L"Modified");
@@ -943,6 +1044,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -952,11 +1054,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestUpdateButtonsUnpublishedChanges)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
 
@@ -967,6 +1071,7 @@ public:
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -976,12 +1081,14 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Modified");
         vmAssetList.Assets().GetItemAt(2)->UpdateLocalCheckpoint();
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish"), vmAssetList.GetPublishButtonText());
@@ -991,12 +1098,14 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
 
         vmAssetList.Assets().GetItemAt(2)->SetName(L"Ach3");
         vmAssetList.Assets().GetItemAt(2)->UpdateLocalCheckpoint();
         vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
         Assert::AreEqual(std::wstring(L"&Save All"), vmAssetList.GetSaveButtonText());
         Assert::IsFalse(vmAssetList.CanSave());
         Assert::AreEqual(std::wstring(L"&Publish All"), vmAssetList.GetPublishButtonText());
@@ -1006,11 +1115,13 @@ public:
         Assert::AreEqual(std::wstring(L"Re&vert"), vmAssetList.GetRevertButtonText());
         Assert::IsTrue(vmAssetList.CanRevert());
         Assert::IsTrue(vmAssetList.CanClone());
+        Assert::IsTrue(vmAssetList.CanCreate());
     }
 
     TEST_METHOD(TestActivateSelectedSingle)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
 
@@ -1025,16 +1136,19 @@ public:
         vmAssetList.ForceUpdateButtons();
         Assert::AreEqual(AssetState::Waiting, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
 
         vmAssetList.ActivateSelected();
         vmAssetList.ForceUpdateButtons();
         Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
     }
 
     TEST_METHOD(TestActivateSelectedMultiple)
     {
         AssetListViewModelHarness vmAssetList;
+        vmAssetList.SetGameId(1U);
         vmAssetList.SetFilterCategory(AssetCategory::Core);
         vmAssetList.AddThreeAchievements();
         vmAssetList.Assets().GetItemAt(2)->SetState(AssetState::Active);
@@ -1053,12 +1167,14 @@ public:
         Assert::AreEqual(AssetState::Waiting, vmAssetList.FilteredAssets().GetItemAt(0)->GetState());
         Assert::AreEqual(AssetState::Active, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
         Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
 
         vmAssetList.ActivateSelected();
         vmAssetList.ForceUpdateButtons();
         Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(0)->GetState());
         Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
         Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
     }
 
     TEST_METHOD(TestSaveSelectedLocalUnmodified)
@@ -1234,6 +1350,62 @@ public:
         vmAssetList.DoFrame();
         Assert::AreEqual(AssetState::Active, pAchievement->GetState());
     }
+
+    TEST_METHOD(TestCreateNew)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockGameContext.SetGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1");
+
+        Assert::AreEqual({ 2U }, vmAssetList.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Core, vmAssetList.GetFilterCategory());
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        vmAssetList.CreateNew();
+
+        // new Local achievement should be created and focused
+        Assert::AreEqual({ 3U }, vmAssetList.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Local, vmAssetList.GetFilterCategory());
+
+        const auto* pAsset = vmAssetList.FilteredAssets().GetItemAt(0);
+        Expects(pAsset != nullptr);
+        Assert::IsTrue(pAsset->IsSelected());
+        Assert::AreEqual(std::wstring(), pAsset->GetLabel());
+        Assert::AreEqual(AssetCategory::Local, pAsset->GetCategory());
+        Assert::AreEqual(AssetState::Inactive, pAsset->GetState());
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+        Assert::AreEqual({ 111000001U }, pAsset->GetId());
+        Assert::AreEqual(0, pAsset->GetPoints());
+
+        // and loaded in the editor, which should be shown (local achievement will always have ID 0)
+        Assert::AreEqual({ 0U }, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAssetLoaded());
+        Assert::IsTrue(bEditorShown);
+        bEditorShown = false;
+
+        // second new Local achievement should be focused, deselecting the first
+        vmAssetList.CreateNew();
+        Assert::AreEqual({ 4U }, vmAssetList.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Local, vmAssetList.GetFilterCategory());
+
+        pAsset = vmAssetList.FilteredAssets().GetItemAt(0);
+        Expects(pAsset != nullptr);
+        Assert::IsFalse(pAsset->IsSelected());
+
+        pAsset = vmAssetList.FilteredAssets().GetItemAt(1);
+        Expects(pAsset != nullptr);
+        Assert::IsTrue(pAsset->IsSelected());
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/BrokenAchievementsViewModel_Tests.cpp b/tests/ui/viewmodels/BrokenAchievementsViewModel_Tests.cpp
index 7fdb4628..800e857b 100644
--- a/tests/ui/viewmodels/BrokenAchievementsViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/BrokenAchievementsViewModel_Tests.cpp
@@ -6,6 +6,8 @@
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockServer.hh"
+#include "tests\mocks\MockThreadPool.hh"
+#include "tests\mocks\MockWindowManager.hh"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
 
@@ -24,7 +26,9 @@ private:
     public:
         ra::api::mocks::MockServer mockServer;
         ra::data::mocks::MockGameContext mockGameContext;
+        ra::services::mocks::MockThreadPool mockThreadPool;
         ra::ui::mocks::MockDesktop mockDesktop;
+        ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
         void MockAchievements()
         {
diff --git a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
index f5683328..305e5513 100644
--- a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
@@ -10,6 +10,7 @@
 #include "tests\mocks\MockSessionTracker.hh"
 #include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
+#include "tests\mocks\MockWindowManager.hh"
 #include "tests\RA_UnitTestHelpers.h"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
@@ -33,6 +34,7 @@ private:
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::ui::mocks::MockImageRepository mockImageRepository;
         ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager;
+        ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
         ItemViewModel* GetItem(gsl::index nIndex) { return m_vItems.GetItemAt(nIndex); }