diff --git a/src/ui/viewmodels/MemoryViewerViewModel.cpp b/src/ui/viewmodels/MemoryViewerViewModel.cpp index b44ca9e8e..4191e6265 100644 --- a/src/ui/viewmodels/MemoryViewerViewModel.cpp +++ b/src/ui/viewmodels/MemoryViewerViewModel.cpp @@ -23,6 +23,7 @@ constexpr uint8_t HIGHLIGHTED_COLOR = STALE_COLOR | gsl::narrow_cast(ra constexpr int ADDRESS_COLUMN_WIDTH = 10; constexpr int BASE_MEMORY_VIEWER_WIDTH_IN_CHARACTERS = (ADDRESS_COLUMN_WIDTH + 16 * 3 - 1); // address column + 16 bytes "XX " - last space +constexpr int MEMVIEW_HISTORY_MAX_SIZE = 50; std::unique_ptr MemoryViewerViewModel::s_pFontSurface; std::unique_ptr MemoryViewerViewModel::s_pFontASCIISurface; @@ -94,6 +95,8 @@ class MemoryViewerViewModel::MemoryBookmarkMonitor : protected ViewModelCollecti private: ViewModelCollection& m_vBookmarks; MemoryViewerViewModel& m_pOwner; + std::vector vHistory; + std::vector::iterator iterHistoryIndex; }; MemoryViewerViewModel::MemoryViewerViewModel() @@ -110,6 +113,9 @@ MemoryViewerViewModel::MemoryViewerViewModel() m_pInvalid = m_pMemory + MaxLines * 16 * 2; memset(m_pInvalid, 0, MaxLines * 16); + + vHistory = {0}; + iterHistoryIndex = vHistory.begin(); } void MemoryViewerViewModel::InitializeNotifyTargets() @@ -366,6 +372,8 @@ void MemoryViewerViewModel::SetAddress(ra::ByteAddress nValue) { SetValue(PendingAddressProperty, gsl::narrow_cast(nValue)); } + + AddToHistory(nValue); } void MemoryViewerViewModel::SetFirstAddress(ra::ByteAddress value) @@ -666,6 +674,8 @@ void MemoryViewerViewModel::OnActiveGameChanged() const auto& pGameContext = ra::services::ServiceLocator::Get(); m_bReadOnly = (pGameContext.GameId() == 0); + + ClearHistory(); } void MemoryViewerViewModel::OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring& sNote) @@ -1314,6 +1324,40 @@ void MemoryViewerViewModel::RenderHeader() } } +void MemoryViewerViewModel::AddToHistory(ra::ByteAddress nAddress) +{ + if (*iterHistoryIndex != nAddress) + { + vHistory.erase(iterHistoryIndex + 1, vHistory.end()); + if (vHistory.size() == MEMVIEW_HISTORY_MAX_SIZE) + vHistory.erase(vHistory.begin()); + vHistory.push_back(nAddress); + iterHistoryIndex = vHistory.end() - 1; + } +} + +void MemoryViewerViewModel::ClearHistory() +{ + vHistory.erase(vHistory.begin(), vHistory.end()); + vHistory.push_back(0); + iterHistoryIndex = vHistory.begin(); +} + +void MemoryViewerViewModel::MoveHistoryForward() +{ + int nIndex = std::distance(vHistory.begin(), iterHistoryIndex); + if (vHistory.size() > (nIndex + 1)) + ++iterHistoryIndex; + SetAddress(*iterHistoryIndex); +} + +void MemoryViewerViewModel::MoveHistoryBackward() +{ + if (vHistory.size() >= 1 and iterHistoryIndex != vHistory.begin()) + --iterHistoryIndex; + SetAddress(*iterHistoryIndex); +} + } // namespace viewmodels } // namespace ui } // namespace ra diff --git a/src/ui/viewmodels/MemoryViewerViewModel.hh b/src/ui/viewmodels/MemoryViewerViewModel.hh index f295118cc..8b2d9d70e 100644 --- a/src/ui/viewmodels/MemoryViewerViewModel.hh +++ b/src/ui/viewmodels/MemoryViewerViewModel.hh @@ -175,6 +175,11 @@ public: void AdvanceCursorPage(); void RetreatCursorPage(); + void AddToHistory(ra::ByteAddress nAddress); + void ClearHistory(); + void MoveHistoryBackward(); + void MoveHistoryForward(); + protected: void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; @@ -206,6 +211,8 @@ protected: static constexpr int REDRAW_ALL = REDRAW_MEMORY | REDRAW_ADDRESSES | REDRAW_HEADERS; int m_nNeedsRedraw = REDRAW_ALL; + + static constexpr int MaxLines = 128; private: @@ -239,6 +246,9 @@ private: class MemoryBookmarkMonitor; friend class MemoryBookmarkMonitor; std::unique_ptr m_pBookmarkMonitor; + + std::vector vHistory; + std::vector::iterator iterHistoryIndex; }; } // namespace viewmodels diff --git a/src/ui/win32/bindings/MemoryViewerControlBinding.cpp b/src/ui/win32/bindings/MemoryViewerControlBinding.cpp index 9cf8128d0..930974309 100644 --- a/src/ui/win32/bindings/MemoryViewerControlBinding.cpp +++ b/src/ui/win32/bindings/MemoryViewerControlBinding.cpp @@ -57,6 +57,13 @@ INT_PTR CALLBACK MemoryViewerControlBinding::WndProc(HWND hControl, UINT uMsg, W case WM_USER_INVALIDATE: Invalidate(); return FALSE; + + case WM_XBUTTONUP: + if (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) + m_pViewModel.MoveHistoryBackward(); + else if (GET_XBUTTON_WPARAM(wParam) == XBUTTON2) + m_pViewModel.MoveHistoryForward(); + return FALSE; } return ControlBinding::WndProc(hControl, uMsg, wParam, lParam); diff --git a/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp b/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp index b900ee33b..81d16f2ba 100644 --- a/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp +++ b/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp @@ -1918,6 +1918,54 @@ TEST_CLASS(MemoryViewerViewModel_Tests) Assert::IsTrue(viewer.NeedsRedraw()); viewer.MockRender(); } + TEST_METHOD(TestOnMoveHistory) + { + MemoryViewerViewModelHarness viewer; + viewer.InitializeMemory(256); + + viewer.SetAddress(0U); + viewer.SetReadOnly(false); + + // Step backward one time should do nothing as it's the first move + viewer.MoveHistoryBackward(); + Assert::AreEqual({0U}, viewer.GetAddress()); + + // Move to 0x1 and step backward should lead to address 0x0 + viewer.SetAddress(1U); + viewer.MoveHistoryBackward(); + Assert::AreEqual({0U}, viewer.GetAddress()); + + // Move one step forward should lead to address 0x1 + viewer.MoveHistoryForward(); + Assert::AreEqual({1U}, viewer.GetAddress()); + + // Move to 0x1 then to 0x2 and finally to 0x3 and step backward one time should lead to address 0x2 + viewer.SetAddress(1U); + viewer.SetAddress(2U); + viewer.SetAddress(3U); + viewer.MoveHistoryBackward(); + Assert::AreEqual({2U}, viewer.GetAddress()); + + // Move two steps forward should lead to address 0x3 as it's the most recent history entry + viewer.MoveHistoryForward(); + viewer.MoveHistoryForward(); + Assert::AreEqual({3U}, viewer.GetAddress()); + + // Three last steps backward and we are back to square one at address 0x0 + viewer.MoveHistoryBackward(); + viewer.MoveHistoryBackward(); + viewer.MoveHistoryBackward(); + Assert::AreEqual({0U}, viewer.GetAddress()); + + // Change game to clear history + viewer.mockGameContext.NotifyActiveGameChanged(); + + // Move three steps forward to get back to 0x3 should do nothing as the game changed and history got cleared + viewer.MoveHistoryForward(); + viewer.MoveHistoryForward(); + viewer.MoveHistoryForward(); + Assert::AreEqual({0U}, viewer.GetAddress()); + } }; } // namespace tests