From 32dd2ff713c9ec39ef0019528a0280ab9eddb247 Mon Sep 17 00:00:00 2001 From: leoshone Date: Sat, 5 Sep 2026 22:24:39 +0800 Subject: [PATCH 1/7] Persist the tree view zoom level across Notepad++ sessions (#251) * Persist the tree view zoom level across Notepad++ sessions The plugin already ships a zoom slider for the JSON tree (80%..250%), but the chosen level only lived in the slider control: closing Notepad++ and starting it again always fell back to 100%. Store the zoom percentage in JSONViewer.ini under [Others] TREE_ZOOM and re-apply it when the dialog is initialised. The value is written only when it actually changes, and while the slider thumb is being dragged (TB_THUMBTRACK) nothing is written, so a drag gesture produces a single write at the end instead of one per pixel. Purely additive: the existing ini keys and the default behaviour are untouched. * Fix the drag detection of the zoom slider WM_HSCROLL carries the notification code in LOWORD(wParam), not HIWORD: HIWORD holds the thumb position itself (80..250 here), so comparing it against TB_THUMBTRACK never matched and the zoom was written to the ini file continuously while the thumb was being dragged, instead of once when the gesture ended. Found by an independent review of the integration branch; the end-to-end harness never caught it because it only sends TB_ENDTRACK and never simulates the dragging itself. --------- Co-authored-by: leoshone --- src/NppJsonViewer/Define.h | 6 ++++++ src/NppJsonViewer/JsonViewDlg.cpp | 27 +++++++++++++++++++++++++++ src/NppJsonViewer/JsonViewDlg.h | 1 + src/NppJsonViewer/NppJsonPlugin.cpp | 3 ++- src/NppJsonViewer/Profile.cpp | 5 +++++ tests/UnitTest/ProfileTest.cpp | 24 ++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/NppJsonViewer/Define.h b/src/NppJsonViewer/Define.h index 5ee80f1..3834ddb 100644 --- a/src/NppJsonViewer/Define.h +++ b/src/NppJsonViewer/Define.h @@ -1,4 +1,6 @@ #pragma once +#include + #include "PluginInterface.h" // Define the number of plugin commands here @@ -64,6 +66,7 @@ const TCHAR STR_INI_FORMATTING_INDENTCOUNT[] = TEXT("INDENTATION_COUNT"); const TCHAR STR_INI_OTHER_SEC[] = TEXT("Others"); const TCHAR STR_INI_OTHER_FOLLOW_TAB[] = TEXT("FOLLOW_TAB"); +const TCHAR STR_INI_OTHER_TREE_ZOOM[] = TEXT("TREE_ZOOM"); const TCHAR STR_INI_OTHER_AUTO_FORMAT[] = TEXT("AUTO_FORMAT"); const TCHAR STR_INI_OTHER_USE_HIGHLIGHT[] = TEXT("USE_JSON_HIGHLIGHT"); const TCHAR STR_INI_OTHER_IGNORE_COMMENT[] = TEXT("IGNORE_COMMENT"); @@ -117,4 +120,7 @@ struct Setting bool bAutoFormat = false; bool bUseJsonHighlight = true; ParseOptions parseOptions {}; + int nTreeZoom = 100; // Tree view font zoom in percent (80..250) + + std::wstring configPath; // Full path of JSONViewer.ini (not persisted) }; diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index 4080af0..331d2d4 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -936,6 +936,19 @@ void JsonViewDlg::UpdateUIOnZoom(int zoomPercentage) const SetTreeViewZoom(zoomFactor); } +void JsonViewDlg::PersistZoom(int zoomPercentage) +{ + const auto& zoomRange = m_pTreeViewZoom->GetRange(); + if (zoomPercentage < zoomRange.m_nMinZoom || zoomPercentage > zoomRange.m_nMaxZoom) + return; + + if (m_pSetting->nTreeZoom != zoomPercentage) + { + m_pSetting->nTreeZoom = zoomPercentage; + ProfileSetting(m_pSetting->configPath).SetSettings(*m_pSetting); + } +} + void JsonViewDlg::HandleZoomOnScroll(WPARAM wParam) const { int pos = GetZoomLevel(); // Current zoom level @@ -1101,6 +1114,10 @@ INT_PTR JsonViewDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam) m_pTreeView->OnInit(getHSelf(), IDC_TREE); m_pTreeViewZoom->OnInit(getHSelf(), IDC_ZOOM_SLIDER, IDC_ZOOM_PERCENT); + // Apply the zoom level restored from JSONViewer.ini + const auto& zoomRange = m_pTreeViewZoom->GetRange(); + UpdateUIOnZoom(std::clamp(m_pSetting->nTreeZoom, zoomRange.m_nMinZoom, zoomRange.m_nMaxZoom)); + PrepareButtons(); // Set default node path as JSON @@ -1182,6 +1199,7 @@ INT_PTR JsonViewDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam) if (GetKeyState(VK_CONTROL) & 0x8000) { HandleZoomOnScroll(wParam); + PersistZoom(GetZoomLevel()); return TRUE; } return FALSE; @@ -1193,9 +1211,18 @@ INT_PTR JsonViewDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam) if (reinterpret_cast(lParam) == hSlider) { + // While the thumb is being dragged (TB_THUMBTRACK) the position + // changes continuously, so only persist once the gesture is over. + // WM_HSCROLL carries the notification code in LOWORD(wParam); + // HIWORD is the thumb position itself. + const bool bDragging = (LOWORD(wParam) == TB_THUMBTRACK); + int pos = m_pTreeViewZoom->GetPosition(); UpdateUIOnZoom(pos); + if (!bDragging) + PersistZoom(pos); + return TRUE; } return FALSE; diff --git a/src/NppJsonViewer/JsonViewDlg.h b/src/NppJsonViewer/JsonViewDlg.h index 00cc599..605db34 100644 --- a/src/NppJsonViewer/JsonViewDlg.h +++ b/src/NppJsonViewer/JsonViewDlg.h @@ -95,6 +95,7 @@ class JsonViewDlg void SetTreeViewZoom(double dwZoomFactor) const; void UpdateUIOnZoom(int zoomPercentage) const; void HandleZoomOnScroll(WPARAM wParam) const; + void PersistZoom(int zoomPercentage); void HandleTreeEvents(LPARAM lParam) const; diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index f251481..e7224ff 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -169,7 +169,8 @@ void NppJsonPlugin::ConstructSetting() { if (!m_pSetting) { - m_pSetting = std::make_shared(); + m_pSetting = std::make_shared(); + m_pSetting->configPath = m_configPath; ProfileSetting(m_configPath).GetSettings(*m_pSetting); } } diff --git a/src/NppJsonViewer/Profile.cpp b/src/NppJsonViewer/Profile.cpp index 7f13a18..cf6c9c6 100644 --- a/src/NppJsonViewer/Profile.cpp +++ b/src/NppJsonViewer/Profile.cpp @@ -94,6 +94,10 @@ bool ProfileSetting::GetSettings(Setting& info) const if (bRetVal) info.bFollowCurrentTab = static_cast(nVal); + bRetVal = bRetVal && ReadValue(STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM, nVal, info.nTreeZoom); + if (bRetVal) + info.nTreeZoom = nVal; + bRetVal = bRetVal && ReadValue(STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT, nVal, info.bAutoFormat); if (bRetVal) info.bAutoFormat = static_cast(nVal); @@ -127,6 +131,7 @@ bool ProfileSetting::SetSettings(const Setting& info) const bRetVal = bRetVal && WriteValue(STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENTCOUNT, info.indent.len); bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_FOLLOW_TAB, info.bFollowCurrentTab); + bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM, info.nTreeZoom); bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT, info.bAutoFormat); bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_USE_HIGHLIGHT, info.bUseJsonHighlight); bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMENT, info.parseOptions.bIgnoreComment); diff --git a/tests/UnitTest/ProfileTest.cpp b/tests/UnitTest/ProfileTest.cpp index 10ab846..873d9f6 100644 --- a/tests/UnitTest/ProfileTest.cpp +++ b/tests/UnitTest/ProfileTest.cpp @@ -137,6 +137,7 @@ namespace ProfileSettingTests EXPECT_EQ(setting.bFollowCurrentTab, false); EXPECT_EQ(setting.bAutoFormat, false); EXPECT_EQ(setting.bUseJsonHighlight, true); + EXPECT_EQ(setting.nTreeZoom, 100); EXPECT_EQ(setting.parseOptions.bIgnoreComment, true); EXPECT_EQ(setting.parseOptions.bIgnoreTrailingComma, true); @@ -173,9 +174,32 @@ namespace ProfileSettingTests EXPECT_EQ(actual.bFollowCurrentTab, expected.bFollowCurrentTab); EXPECT_EQ(actual.bAutoFormat, expected.bAutoFormat); EXPECT_EQ(actual.bUseJsonHighlight, expected.bUseJsonHighlight); + EXPECT_EQ(actual.nTreeZoom, expected.nTreeZoom); EXPECT_EQ(actual.parseOptions.bIgnoreComment, expected.parseOptions.bIgnoreComment); EXPECT_EQ(actual.parseOptions.bIgnoreTrailingComma, expected.parseOptions.bIgnoreTrailingComma); EXPECT_EQ(actual.parseOptions.bReplaceUndefined, expected.parseOptions.bReplaceUndefined); } + + TEST_F(ProfileTest, TreeZoom_RoundTrip) + { + // a profile without the TREE_ZOOM key falls back to 100% + { + Setting setting {}; + EXPECT_TRUE(m_pProfile->GetSettings(setting)); + EXPECT_EQ(setting.nTreeZoom, 100); + } + + // every value inside the slider range must survive a write/read cycle + for (int zoom : { 80, 100, 150, 200, 250 }) + { + Setting expected {}; + expected.nTreeZoom = zoom; + ASSERT_TRUE(m_pProfile->SetSettings(expected)) << zoom; + + Setting actual {}; + ASSERT_TRUE(m_pProfile->GetSettings(actual)) << zoom; + EXPECT_EQ(actual.nTreeZoom, zoom); + } + } } // namespace ProfileSettingTests From 0f096dc9ea818eb27457e01a2f2c66bb33a0410c Mon Sep 17 00:00:00 2001 From: Rjaendra Singh Date: Sun, 6 Sep 2026 01:53:22 +0530 Subject: [PATCH 2/7] Code improvement 1. Set json viewer setting on exit that too if it is changed. 2. Header order and minor correction --- src/NppJsonViewer/AboutDlg.h | 1 + src/NppJsonViewer/Define.h | 7 +++-- src/NppJsonViewer/JsonNode.h | 1 + src/NppJsonViewer/JsonViewDlg.cpp | 6 +---- src/NppJsonViewer/NppJsonPlugin.cpp | 14 +++++++--- src/NppJsonViewer/NppJsonPlugin.h | 3 ++- src/NppJsonViewer/Profile.cpp | 40 +++++++++++++++++++---------- src/NppJsonViewer/Profile.h | 2 ++ src/NppJsonViewer/ScintillaEditor.h | 4 ++- src/NppJsonViewer/SettingsDlg.h | 7 +++-- src/NppJsonViewer/ShortcutCommand.h | 5 +++- src/NppJsonViewer/SliderCtrl.h | 1 - src/NppJsonViewer/TreeViewCtrl.h | 1 - 13 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/NppJsonViewer/AboutDlg.h b/src/NppJsonViewer/AboutDlg.h index 163969e..0b00522 100644 --- a/src/NppJsonViewer/AboutDlg.h +++ b/src/NppJsonViewer/AboutDlg.h @@ -1,4 +1,5 @@ #pragma once + #include "DockingFeature/StaticDialog.h" class AboutDlg : public StaticDialog diff --git a/src/NppJsonViewer/Define.h b/src/NppJsonViewer/Define.h index 3834ddb..0e2a365 100644 --- a/src/NppJsonViewer/Define.h +++ b/src/NppJsonViewer/Define.h @@ -1,4 +1,5 @@ #pragma once + #include #include "PluginInterface.h" @@ -66,7 +67,7 @@ const TCHAR STR_INI_FORMATTING_INDENTCOUNT[] = TEXT("INDENTATION_COUNT"); const TCHAR STR_INI_OTHER_SEC[] = TEXT("Others"); const TCHAR STR_INI_OTHER_FOLLOW_TAB[] = TEXT("FOLLOW_TAB"); -const TCHAR STR_INI_OTHER_TREE_ZOOM[] = TEXT("TREE_ZOOM"); +const TCHAR STR_INI_OTHER_TREE_ZOOM[] = TEXT("TREE_ZOOM_LEVEL"); const TCHAR STR_INI_OTHER_AUTO_FORMAT[] = TEXT("AUTO_FORMAT"); const TCHAR STR_INI_OTHER_USE_HIGHLIGHT[] = TEXT("USE_JSON_HIGHLIGHT"); const TCHAR STR_INI_OTHER_IGNORE_COMMENT[] = TEXT("IGNORE_COMMENT"); @@ -119,8 +120,6 @@ struct Setting bool bFollowCurrentTab = false; bool bAutoFormat = false; bool bUseJsonHighlight = true; - ParseOptions parseOptions {}; int nTreeZoom = 100; // Tree view font zoom in percent (80..250) - - std::wstring configPath; // Full path of JSONViewer.ini (not persisted) + ParseOptions parseOptions {}; }; diff --git a/src/NppJsonViewer/JsonNode.h b/src/NppJsonViewer/JsonNode.h index 0ef396a..fb47a8d 100644 --- a/src/NppJsonViewer/JsonNode.h +++ b/src/NppJsonViewer/JsonNode.h @@ -1,4 +1,5 @@ #pragma once + #include enum class JsonNodeType : short diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index 331d2d4..8e19710 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -942,11 +942,7 @@ void JsonViewDlg::PersistZoom(int zoomPercentage) if (zoomPercentage < zoomRange.m_nMinZoom || zoomPercentage > zoomRange.m_nMaxZoom) return; - if (m_pSetting->nTreeZoom != zoomPercentage) - { - m_pSetting->nTreeZoom = zoomPercentage; - ProfileSetting(m_pSetting->configPath).SetSettings(*m_pSetting); - } + m_pSetting->nTreeZoom = zoomPercentage; } void JsonViewDlg::HandleZoomOnScroll(WPARAM wParam) const diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index e7224ff..2b302df 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -1,7 +1,8 @@ +#include + #include "NppJsonPlugin.h" #include "resource.h" #include "Profile.h" -#include NppJsonPlugin* NppJsonPlugin::Callback::m_pNppJsonPlugin = nullptr; @@ -16,7 +17,13 @@ void NppJsonPlugin::PluginInit(HMODULE hModule) m_hModule = hModule; } -void NppJsonPlugin::PluginCleanup() {} +void NppJsonPlugin::PluginCleanup() +{ + if (m_pSetting) + { + ProfileSetting(m_configPath).SetSettings(*m_pSetting); + } +} void NppJsonPlugin::SetInfo(const NppData& nppData) { @@ -169,8 +176,7 @@ void NppJsonPlugin::ConstructSetting() { if (!m_pSetting) { - m_pSetting = std::make_shared(); - m_pSetting->configPath = m_configPath; + m_pSetting = std::make_shared(); ProfileSetting(m_configPath).GetSettings(*m_pSetting); } } diff --git a/src/NppJsonViewer/NppJsonPlugin.h b/src/NppJsonViewer/NppJsonPlugin.h index f0589a7..ab1b17a 100644 --- a/src/NppJsonViewer/NppJsonPlugin.h +++ b/src/NppJsonViewer/NppJsonPlugin.h @@ -1,7 +1,8 @@ -#pragma +#pragma once #include #include + #include "Define.h" #include "Notepad_plus_msgs.h" #include "ShortcutCommand.h" diff --git a/src/NppJsonViewer/Profile.cpp b/src/NppJsonViewer/Profile.cpp index cf6c9c6..ce86df0 100644 --- a/src/NppJsonViewer/Profile.cpp +++ b/src/NppJsonViewer/Profile.cpp @@ -1,9 +1,11 @@ +#include +#include + #include "Profile.h" #include "Utility.h" #include "Define.h" #include "StringHelper.h" -#include -#include + Profile::Profile(const std::wstring& path) : m_ProfileFilePath(path) @@ -123,20 +125,30 @@ bool ProfileSetting::GetSettings(Setting& info) const bool ProfileSetting::SetSettings(const Setting& info) const { + Setting current; + + if (!GetSettings(current)) + return false; + bool bRetVal = true; - bRetVal = bRetVal && WriteValue(STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_EOL, static_cast(info.lineEnding)); - bRetVal = bRetVal && WriteValue(STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_LINE, static_cast(info.lineFormat)); - bRetVal = bRetVal && WriteValue(STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENT, static_cast(info.indent.style)); - bRetVal = bRetVal && WriteValue(STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENTCOUNT, info.indent.len); - - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_FOLLOW_TAB, info.bFollowCurrentTab); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM, info.nTreeZoom); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT, info.bAutoFormat); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_USE_HIGHLIGHT, info.bUseJsonHighlight); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMENT, info.parseOptions.bIgnoreComment); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMA, info.parseOptions.bIgnoreTrailingComma); - bRetVal = bRetVal && WriteValue(STR_INI_OTHER_SEC, STR_INI_OTHER_REPLACE_UNDEFINED, info.parseOptions.bReplaceUndefined); + auto writeIfChanged = [&](const auto& oldValue, const auto& newValue, const std::wstring& section, const std::wstring& key) + { + if (oldValue != newValue) + bRetVal = bRetVal && WriteValue(section, key, static_cast(newValue)); + }; + + writeIfChanged(current.lineEnding, info.lineEnding, STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_EOL); + writeIfChanged(current.lineFormat, info.lineFormat, STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_LINE); + writeIfChanged(current.indent.style, info.indent.style, STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENT); + writeIfChanged(current.indent.len, info.indent.len, STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENTCOUNT); + writeIfChanged(current.bFollowCurrentTab, info.bFollowCurrentTab, STR_INI_OTHER_SEC, STR_INI_OTHER_FOLLOW_TAB); + writeIfChanged(current.nTreeZoom, info.nTreeZoom, STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM); + writeIfChanged(current.bAutoFormat, info.bAutoFormat, STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT); + writeIfChanged(current.bUseJsonHighlight, info.bUseJsonHighlight, STR_INI_OTHER_SEC, STR_INI_OTHER_USE_HIGHLIGHT); + writeIfChanged(current.parseOptions.bIgnoreComment, info.parseOptions.bIgnoreComment, STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMENT); + writeIfChanged(current.parseOptions.bIgnoreTrailingComma, info.parseOptions.bIgnoreTrailingComma, STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMA); + writeIfChanged(current.parseOptions.bReplaceUndefined, info.parseOptions.bReplaceUndefined, STR_INI_OTHER_SEC, STR_INI_OTHER_REPLACE_UNDEFINED); return bRetVal; } diff --git a/src/NppJsonViewer/Profile.h b/src/NppJsonViewer/Profile.h index 54acd50..f938b88 100644 --- a/src/NppJsonViewer/Profile.h +++ b/src/NppJsonViewer/Profile.h @@ -1,5 +1,7 @@ #pragma once + #include + #include "Define.h" class Profile diff --git a/src/NppJsonViewer/ScintillaEditor.h b/src/NppJsonViewer/ScintillaEditor.h index b3dd806..2a183bb 100644 --- a/src/NppJsonViewer/ScintillaEditor.h +++ b/src/NppJsonViewer/ScintillaEditor.h @@ -1,9 +1,11 @@ #pragma once -#include "Define.h" + #include #include #include +#include "Define.h" + enum class ScintillaCode : short { Unknown, diff --git a/src/NppJsonViewer/SettingsDlg.h b/src/NppJsonViewer/SettingsDlg.h index 0f20190..cb36f95 100644 --- a/src/NppJsonViewer/SettingsDlg.h +++ b/src/NppJsonViewer/SettingsDlg.h @@ -1,9 +1,12 @@ #pragma once -#include "DockingFeature/StaticDialog.h" -#include "Define.h" + #include #include +#include "DockingFeature/StaticDialog.h" +#include "Define.h" + + class SettingsDlg : public StaticDialog { public: diff --git a/src/NppJsonViewer/ShortcutCommand.h b/src/NppJsonViewer/ShortcutCommand.h index a9a82f7..dab935b 100644 --- a/src/NppJsonViewer/ShortcutCommand.h +++ b/src/NppJsonViewer/ShortcutCommand.h @@ -1,7 +1,10 @@ #pragma once -#include "Define.h" + #include +#include "Define.h" + + class ShortcutCommand { public: diff --git a/src/NppJsonViewer/SliderCtrl.h b/src/NppJsonViewer/SliderCtrl.h index e8e3c74..2fe941d 100644 --- a/src/NppJsonViewer/SliderCtrl.h +++ b/src/NppJsonViewer/SliderCtrl.h @@ -1,6 +1,5 @@ #pragma once - #include #include diff --git a/src/NppJsonViewer/TreeViewCtrl.h b/src/NppJsonViewer/TreeViewCtrl.h index c5153af..198a5be 100644 --- a/src/NppJsonViewer/TreeViewCtrl.h +++ b/src/NppJsonViewer/TreeViewCtrl.h @@ -1,7 +1,6 @@ #pragma once #include - #include #include From 7605bf12cbe2943ca4dc7db9c97c92ec46f204fe Mon Sep 17 00:00:00 2001 From: Rjaendra Singh Date: Mon, 7 Sep 2026 01:11:43 +0530 Subject: [PATCH 3/7] Update sumodule --- external/googletest | 2 +- external/npp_plugintemplate | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/googletest b/external/googletest index 94be250..283c175 160000 --- a/external/googletest +++ b/external/googletest @@ -1 +1 @@ -Subproject commit 94be250af7e14c58dcbf476972d2d7141551ff67 +Subproject commit 283c17563fe7a1111cd7f581aa5d541e8baeff2f diff --git a/external/npp_plugintemplate b/external/npp_plugintemplate index 05ee8e9..1b1fa44 160000 --- a/external/npp_plugintemplate +++ b/external/npp_plugintemplate @@ -1 +1 @@ -Subproject commit 05ee8e9a501e1e37ba462638df188837741c1f78 +Subproject commit 1b1fa44119f132820d6381f8d6660e970799f8a0 From fa8d2b9317738031a672595534c6495adb0b5068 Mon Sep 17 00:00:00 2001 From: Rajendra Singh Date: Mon, 7 Sep 2026 01:37:33 +0530 Subject: [PATCH 4/7] Header file oder change. --- src/NppJsonViewer/AboutDlg.cpp | 6 ++++-- src/NppJsonViewer/JsonHandler.cpp | 4 ++-- src/NppJsonViewer/JsonViewDlg.cpp | 3 ++- src/NppJsonViewer/NppJsonPlugin.cpp | 4 ++-- src/NppJsonViewer/NppJsonPlugin.h | 2 +- src/NppJsonViewer/Profile.cpp | 3 ++- src/NppJsonViewer/ScintillaEditor.cpp | 1 + src/NppJsonViewer/SettingsDlg.cpp | 6 ++++-- src/NppJsonViewer/ShortcutCommand.cpp | 1 + src/NppJsonViewer/TreeViewCtrl.cpp | 3 ++- 10 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/NppJsonViewer/AboutDlg.cpp b/src/NppJsonViewer/AboutDlg.cpp index a9241b4..6b5a5fd 100644 --- a/src/NppJsonViewer/AboutDlg.cpp +++ b/src/NppJsonViewer/AboutDlg.cpp @@ -1,10 +1,12 @@ #include "AboutDlg.h" + +#include +#include + #include "resource.h" #include "Utility.h" #include "StringHelper.h" #include "Define.h" -#include -#include AboutDlg::AboutDlg(HINSTANCE hInstance, HWND hParent, int nCmdId) diff --git a/src/NppJsonViewer/JsonHandler.cpp b/src/NppJsonViewer/JsonHandler.cpp index ffd762f..a3f3659 100644 --- a/src/NppJsonViewer/JsonHandler.cpp +++ b/src/NppJsonViewer/JsonHandler.cpp @@ -1,8 +1,8 @@ +#include "JsonHandler.h" + #include #include -#include "JsonHandler.h" - namespace rj = rapidjson; diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index 8e19710..e7b580f 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -1,7 +1,8 @@ +#include "JsonViewDlg.h" + #include #include -#include "JsonViewDlg.h" #include "Define.h" #include "Utility.h" #include "StringHelper.h" diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index 2b302df..40e96c0 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -1,10 +1,10 @@ +#include "NppJsonPlugin.h" + #include -#include "NppJsonPlugin.h" #include "resource.h" #include "Profile.h" -NppJsonPlugin* NppJsonPlugin::Callback::m_pNppJsonPlugin = nullptr; NppJsonPlugin::NppJsonPlugin() : m_shortcutCommands(nTotalCommandCount) diff --git a/src/NppJsonViewer/NppJsonPlugin.h b/src/NppJsonViewer/NppJsonPlugin.h index ab1b17a..cdbaaf0 100644 --- a/src/NppJsonViewer/NppJsonPlugin.h +++ b/src/NppJsonViewer/NppJsonPlugin.h @@ -37,7 +37,7 @@ class NppJsonPlugin class Callback { friend class NppJsonPlugin; - static NppJsonPlugin* m_pNppJsonPlugin; + inline static NppJsonPlugin* m_pNppJsonPlugin = nullptr; public: Callback() = default; diff --git a/src/NppJsonViewer/Profile.cpp b/src/NppJsonViewer/Profile.cpp index ce86df0..1737621 100644 --- a/src/NppJsonViewer/Profile.cpp +++ b/src/NppJsonViewer/Profile.cpp @@ -1,7 +1,8 @@ +#include "Profile.h" + #include #include -#include "Profile.h" #include "Utility.h" #include "Define.h" #include "StringHelper.h" diff --git a/src/NppJsonViewer/ScintillaEditor.cpp b/src/NppJsonViewer/ScintillaEditor.cpp index 4132b5f..613b6f5 100644 --- a/src/NppJsonViewer/ScintillaEditor.cpp +++ b/src/NppJsonViewer/ScintillaEditor.cpp @@ -1,4 +1,5 @@ #include "ScintillaEditor.h" + #include #include diff --git a/src/NppJsonViewer/SettingsDlg.cpp b/src/NppJsonViewer/SettingsDlg.cpp index 8873645..5ff738f 100644 --- a/src/NppJsonViewer/SettingsDlg.cpp +++ b/src/NppJsonViewer/SettingsDlg.cpp @@ -1,9 +1,11 @@ #include "SettingsDlg.h" + +#include +#include + #include "resource.h" #include "Utility.h" #include "Profile.h" -#include -#include SettingsDlg::SettingsDlg(HINSTANCE hInstance, HWND hParent, int nCmdId, const std::wstring& configPath, std::shared_ptr& pSetting) diff --git a/src/NppJsonViewer/ShortcutCommand.cpp b/src/NppJsonViewer/ShortcutCommand.cpp index 018833c..5e6695e 100644 --- a/src/NppJsonViewer/ShortcutCommand.cpp +++ b/src/NppJsonViewer/ShortcutCommand.cpp @@ -1,4 +1,5 @@ #include "ShortcutCommand.h" + #include ShortcutCommand::ShortcutCommand(int nCommandCount) diff --git a/src/NppJsonViewer/TreeViewCtrl.cpp b/src/NppJsonViewer/TreeViewCtrl.cpp index d2ff8fd..ab286be 100644 --- a/src/NppJsonViewer/TreeViewCtrl.cpp +++ b/src/NppJsonViewer/TreeViewCtrl.cpp @@ -1,6 +1,7 @@ +#include "TreeViewCtrl.h" + #include -#include "TreeViewCtrl.h" #include "Define.h" From 8949f279b5b1d998e45cf6b81daef2f67db382fd Mon Sep 17 00:00:00 2001 From: leoshone Date: Thu, 3 Sep 2026 18:37:41 +0800 Subject: [PATCH 5/7] Keep the tree expansion state when refreshing the JSON tree "Refresh JSON Tree" rebuilds every node, so the tree always came back fully collapsed - even when the user only wanted to re-read a document they were already looking at. Capture which nodes are expanded and which one is selected before the tree is thrown away, then re-apply that state onto the freshly built tree, matching nodes by path. Paths that no longer exist (the document changed in the meantime) are silently dropped, and nodes that are new stay collapsed. The state is keyed by node path, which is the list of keys from the tree root down to a node. The pure path arithmetic lives in the new TreeExpansion.h/.cpp so it can be unit tested without a window. DrawJsonTree() gained a bPreserveExpansion parameter that defaults to false, so every other caller (panel opening, formatting, compressing, sorting) keeps behaving exactly as before; only the refresh button opts in. --- src/NppJsonViewer/JsonViewDlg.cpp | 174 +++++++++++++++++- src/NppJsonViewer/JsonViewDlg.h | 21 ++- src/NppJsonViewer/NPPJSONViewer.vcxproj | 2 + .../NPPJSONViewer.vcxproj.filters | 6 + src/NppJsonViewer/TreeExpansion.cpp | 75 ++++++++ src/NppJsonViewer/TreeExpansion.h | 48 +++++ src/NppJsonViewer/TreeViewCtrl.cpp | 10 + src/NppJsonViewer/TreeViewCtrl.h | 6 +- tests/UnitTest/TreeExpansionTest.cpp | 133 +++++++++++++ tests/UnitTest/UnitTest.vcxproj | 3 + 10 files changed, 473 insertions(+), 5 deletions(-) create mode 100644 src/NppJsonViewer/TreeExpansion.cpp create mode 100644 src/NppJsonViewer/TreeExpansion.h create mode 100644 tests/UnitTest/TreeExpansionTest.cpp diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index e7b580f..8d893b5 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -1,6 +1,7 @@ #include "JsonViewDlg.h" #include +#include #include #include "Define.h" @@ -334,7 +335,7 @@ void JsonViewDlg::ValidateJson() DrawJsonTree(); } -void JsonViewDlg::DrawJsonTree() +void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion) { UpdateTitle(); @@ -342,6 +343,13 @@ void JsonViewDlg::DrawJsonTree() std::vector ctrls = {IDC_BTN_REFRESH, IDC_BTN_VALIDATE, IDC_BTN_FORMAT, IDC_BTN_SEARCH, IDC_EDT_SEARCH}; EnableControls(ctrls, false); + // Capture the expansion/selection state before the tree is thrown away, so + // that it can be re-applied onto the freshly built one. + TreeExpansionState expState; + const bool bHasCurrentTree = m_pTreeView->GetRoot() && m_pTreeView->GetNodeCount() > 1; + if (bPreserveExpansion && bHasCurrentTree) + expState = CaptureExpansionState(); + HTREEITEM rootNode = nullptr; rootNode = m_pTreeView->InitTree(); @@ -381,6 +389,11 @@ void JsonViewDlg::DrawJsonTree() m_pTreeView->Expand(rootNode); + // Re-apply the state of the previous tree. Paths that no longer exist + // (the document changed in the meantime) are silently dropped. + if (bPreserveExpansion && bHasCurrentTree && m_pTreeView->GetNodeCount() > 1) + ApplyExpansionState(expState); + // Enable all buttons and treeView EnableControls(ctrls, true); } @@ -566,6 +579,163 @@ void JsonViewDlg::SearchInTree() } } +TreeExpansionState JsonViewDlg::CaptureExpansionState() const +{ + TreeExpansionState expState; + + auto hRoot = m_pTreeView->GetRoot(); + if (!hRoot) + return expState; + + // Collect the key path of every node (root excluded) with its expanded flag + std::function&)> walk; + walk = [&](HTREEITEM hParent, const std::vector& parentKeys) { + for (HTREEITEM hChild = m_pTreeView->GetChildItem(hParent); hChild; + hChild = m_pTreeView->GetNextSibling(hChild)) + { + auto keys = parentKeys; + auto nodeKey = GetPathKey(hChild); + keys.push_back(nodeKey); + + expState.expandedPaths[TreeExpansionHelper::JoinPath(parentKeys, nodeKey)] = m_pTreeView->IsExpanded(hChild); + + walk(hChild, keys); + } + }; + + walk(hRoot, {}); + + expState.selectedPath = GetCurrentSelectedPath(); + + return expState; +} + +void JsonViewDlg::ApplyExpansionState(const TreeExpansionState& state) +{ + auto paths = CollectExpandedPaths(); + auto [pathsToExpand, pathToSelect] = TreeExpansionHelper::MatchExpansion(state, paths); + + for (const auto& path : pathsToExpand) + { + auto keys = TreeExpansionHelper::SplitPath(path); + if (!keys.empty()) + ExpandByPath(keys); + } + + if (!pathToSelect.empty()) + SelectByPath(pathToSelect); +} + +std::vector JsonViewDlg::CollectExpandedPaths() const +{ + std::vector paths; + + auto hRoot = m_pTreeView->GetRoot(); + if (!hRoot) + return paths; + + std::function&)> walk; + walk = [&](HTREEITEM hParent, const std::vector& parentKeys) { + for (HTREEITEM hChild = m_pTreeView->GetChildItem(hParent); hChild; + hChild = m_pTreeView->GetNextSibling(hChild)) + { + auto nodeKey = GetPathKey(hChild); + + auto keys = parentKeys; + keys.push_back(nodeKey); + + paths.push_back(TreeExpansionHelper::JoinPath(parentKeys, nodeKey)); + + walk(hChild, keys); + } + }; + + walk(hRoot, {}); + + return paths; +} + +std::vector JsonViewDlg::GetCurrentSelectedPath() const +{ + std::vector path; + + auto hRoot = m_pTreeView->GetRoot(); + auto hSelected = m_pTreeView->GetSelection(); + if (!hRoot || !hSelected || hSelected == hRoot) + return path; + + // Walk up to the root and reverse the collected keys on the way back + std::vector reversedKeys; + for (HTREEITEM h = hSelected; h && h != hRoot; h = m_pTreeView->GetParentItem(h)) + { + reversedKeys.push_back(GetPathKey(h)); + } + + // Guard against a selection that does not belong to this tree anymore + if (m_pTreeView->GetParentItem(hSelected) == nullptr) + return {}; + + path.assign(reversedKeys.rbegin(), reversedKeys.rend()); + return path; +} + +auto JsonViewDlg::GetPathKey(HTREEITEM hti) const -> std::wstring +{ + auto key = m_pTreeView->GetNodeKey(hti); + + // Remove the surrounding quotes of object keys: "name" -> name. + // Array indices ([0]) and unquoted keys are returned untouched. + if (key.size() >= 2 && key.front() == L'"' && key.back() == L'"') + key = key.substr(1, key.size() - 2); + + return key; +} + +auto JsonViewDlg::FindNodeByPath(const std::vector& path) const -> HTREEITEM +{ + if (path.empty()) + return nullptr; + + auto hRoot = m_pTreeView->GetRoot(); + if (!hRoot) + return nullptr; + + HTREEITEM hCurrent = hRoot; + for (const auto& key : path) + { + HTREEITEM hNext = m_pTreeView->GetChildItem(hCurrent); + while (hNext && GetPathKey(hNext) != key) + { + hNext = m_pTreeView->GetNextSibling(hNext); + } + + if (!hNext) + return nullptr; + + hCurrent = hNext; + } + + return hCurrent == hRoot ? nullptr : hCurrent; +} + +void JsonViewDlg::ExpandByPath(const std::vector& path) +{ + auto hNode = FindNodeByPath(path); + if (hNode) + m_pTreeView->Expand(hNode); +} + +void JsonViewDlg::SelectByPath(const std::vector& path) +{ + auto hNode = FindNodeByPath(path); + if (hNode) + { + // TreeView_SelectItem expands collapsed ancestors on its own, so the + // expansion state restored just before is left untouched. + m_pTreeView->SetSelection(hNode); + } +} + void JsonViewDlg::UpdateTitle() { const auto titleFileName = GetTitleFileName(); @@ -1129,7 +1299,7 @@ INT_PTR JsonViewDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam) { // Handle Button events case IDC_BTN_REFRESH: - DrawJsonTree(); + DrawJsonTree(true); break; case IDC_BTN_FORMAT: diff --git a/src/NppJsonViewer/JsonViewDlg.h b/src/NppJsonViewer/JsonViewDlg.h index 605db34..d33133b 100644 --- a/src/NppJsonViewer/JsonViewDlg.h +++ b/src/NppJsonViewer/JsonViewDlg.h @@ -14,6 +14,7 @@ #include "JsonHandler.h" #include "JsonNode.h" #include "TreeHandler.h" +#include "TreeExpansion.h" class JsonViewDlg @@ -53,7 +54,7 @@ class JsonViewDlg void AppendNodeCount(HTREEITEM node, unsigned elementCount, bool bArray) override; private: - void DrawJsonTree(); + void DrawJsonTree(bool bPreserveExpansion = false); void ReDrawJsonTree(bool bForce = false); void HighlightAsJson(bool bForcefully = false) const; auto PopulateTreeUsingSax(HTREEITEM tree_root, const std::string& jsonText) -> std::optional; @@ -66,6 +67,24 @@ class JsonViewDlg void SearchInTree(); + // Expansion/selection state, captured before the tree is rebuilt and + // re-applied afterwards (see DrawJsonTree(bPreserveExpansion = true)). + auto CaptureExpansionState() const -> TreeExpansionState; + void ApplyExpansionState(const TreeExpansionState& state); + + auto CollectExpandedPaths() const -> std::vector; + auto GetCurrentSelectedPath() const -> std::vector; + void ExpandByPath(const std::vector& path); + void SelectByPath(const std::vector& path); + + // Key of a node as used inside a node path: the raw key without the + // surrounding quotes ("name" -> name, [0] -> [0]). + auto GetPathKey(HTREEITEM hti) const -> std::wstring; + + // Resolve a key path (relative to the tree root) back to a node. + // Returns nullptr when any level of the path cannot be found. + auto FindNodeByPath(const std::vector& path) const -> HTREEITEM; + auto GetTitleFileName() const -> std::wstring; void PrepareButtons(); void SetIconAndTooltip(eButton ctrlType, const std::wstring& toolTip); diff --git a/src/NppJsonViewer/NPPJSONViewer.vcxproj b/src/NppJsonViewer/NPPJSONViewer.vcxproj index fe41d18..1015296 100644 --- a/src/NppJsonViewer/NPPJSONViewer.vcxproj +++ b/src/NppJsonViewer/NPPJSONViewer.vcxproj @@ -197,6 +197,7 @@ + @@ -217,6 +218,7 @@ + diff --git a/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters b/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters index 7193f31..a4b6946 100644 --- a/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters +++ b/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters @@ -57,6 +57,9 @@ Source Files + + Source Files + Source Files @@ -110,6 +113,9 @@ Header Files + + Header Files + Header Files diff --git a/src/NppJsonViewer/TreeExpansion.cpp b/src/NppJsonViewer/TreeExpansion.cpp new file mode 100644 index 0000000..59edeba --- /dev/null +++ b/src/NppJsonViewer/TreeExpansion.cpp @@ -0,0 +1,75 @@ +#include "TreeExpansion.h" + +#include + +auto TreeExpansionHelper::SplitPath(const std::wstring& path) -> std::vector +{ + std::vector keys; + + if (path.empty()) + return keys; + + size_t start = 0; + while (start <= path.size()) + { + auto pos = path.find(L'.', start); + if (pos == std::wstring::npos) + { + keys.emplace_back(path.substr(start)); + break; + } + + keys.emplace_back(path.substr(start, pos - start)); + start = pos + 1; + } + + // An empty trailing key (e.g. a path ending with a dot) is dropped + if (!keys.empty() && keys.back().empty()) + keys.pop_back(); + + return keys; +} + +auto TreeExpansionHelper::JoinPath(const std::vector& parentKeys, const std::wstring& key) -> std::wstring +{ + std::wstring path; + for (const auto& part : parentKeys) + { + path += part; + path += L'.'; + } + path += key; + return path; +} + +auto TreeExpansionHelper::MatchExpansion(const TreeExpansionState& oldState, + const std::vector& newPaths) + -> std::pair, std::vector> +{ + std::vector pathsToExpand; + + for (const auto& path : newPaths) + { + auto find = oldState.expandedPaths.find(path); + if (find != oldState.expandedPaths.cend() && find->second) + pathsToExpand.push_back(path); + } + + std::vector pathToSelect; + if (!oldState.selectedPath.empty()) + { + // Reconstruct the selected path in "joined" form and check existence + std::wstring joined; + for (const auto& key : oldState.selectedPath) + { + if (!joined.empty()) + joined += L'.'; + joined += key; + } + + if (std::find(newPaths.cbegin(), newPaths.cend(), joined) != newPaths.cend()) + pathToSelect = oldState.selectedPath; + } + + return { pathsToExpand, pathToSelect }; +} diff --git a/src/NppJsonViewer/TreeExpansion.h b/src/NppJsonViewer/TreeExpansion.h new file mode 100644 index 0000000..961d835 --- /dev/null +++ b/src/NppJsonViewer/TreeExpansion.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +/* + * TreeExpansionState captures which nodes of the JSON tree are expanded and + * which one is selected, keyed by node path. + * + * It exists so that the state can be re-applied onto a freshly built tree. + * The typical case is "Refresh JSON Tree", which rebuilds every node and + * therefore loses the expansion the user set up. Paths that no longer exist in + * the new tree are simply dropped. + * + * A path is the list of node keys from the tree root down to a node, joined + * with '.' (e.g. "root.child", "root.[0].[1].key"). The tree root itself is not + * part of the path. + */ +struct TreeExpansionState +{ + std::unordered_map expandedPaths; // path -> was expanded + std::vector selectedPath; // key-path of the selected node, empty when none +}; + +class TreeExpansionHelper +{ +public: + // Split a node path into its keys: "root.[0].key" -> { "root", "[0]", "key" } + static auto SplitPath(const std::wstring& path) -> std::vector; + + // Build the path of a node from the path of its parent and the node key. + static auto JoinPath(const std::vector& parentKeys, const std::wstring& key) -> std::wstring; + + /* + * Compute which paths of `oldState` must be re-expanded onto a new tree + * whose node paths are listed in `newPaths`, and where the selection has to + * be restored (only if that path still exists). + * + * Duplicate keys resolve to the first matching node (accepted trade-off: + * JSON object keys are unique in practice, and making this exact would + * require disambiguating sibling order as well). + */ + static auto MatchExpansion(const TreeExpansionState& oldState, + const std::vector& newPaths) + -> std::pair /*pathsToExpand*/, std::vector /*pathToSelect*/>; +}; diff --git a/src/NppJsonViewer/TreeViewCtrl.cpp b/src/NppJsonViewer/TreeViewCtrl.cpp index ab286be..40f1a0b 100644 --- a/src/NppJsonViewer/TreeViewCtrl.cpp +++ b/src/NppJsonViewer/TreeViewCtrl.cpp @@ -49,6 +49,16 @@ auto TreeViewCtrl::InsertNode(const std::wstring& text, LPARAM lparam, HTREEITEM return item; } +auto TreeViewCtrl::GetChildItem(HTREEITEM node) const -> HTREEITEM +{ + return TreeView_GetNextItem(m_hTree, node, TVGN_CHILD); +} + +auto TreeViewCtrl::GetNextSibling(HTREEITEM node) const -> HTREEITEM +{ + return TreeView_GetNextItem(m_hTree, node, TVGN_NEXT); +} + void TreeViewCtrl::UpdateNodeText(HTREEITEM node, const std::wstring& text) { auto tvi = std::make_unique(); diff --git a/src/NppJsonViewer/TreeViewCtrl.h b/src/NppJsonViewer/TreeViewCtrl.h index 198a5be..9bb9edd 100644 --- a/src/NppJsonViewer/TreeViewCtrl.h +++ b/src/NppJsonViewer/TreeViewCtrl.h @@ -29,6 +29,10 @@ class TreeViewCtrl void UpdateNodeText(HTREEITEM node, const std::wstring& text); auto GetNodeCount() const -> unsigned int; + auto GetChildItem(HTREEITEM node) const -> HTREEITEM; + auto GetNextSibling(HTREEITEM node) const -> HTREEITEM; + auto GetParentItem(HTREEITEM node) const -> HTREEITEM; + bool IsExpanded(HTREEITEM node) const; bool IsThisOrAnyChildExpanded(HTREEITEM node) const; bool IsThisOrAnyChildCollapsed(HTREEITEM node) const; @@ -65,8 +69,6 @@ class TreeViewCtrl private: void ExpandOrCollapse(HTREEITEM node, UINT_PTR code) const; - HTREEITEM GetParentItem(HTREEITEM hti) const; - bool GetTVItem(HTREEITEM hti, TVITEM* tvi) const; bool SetTVItem(TVITEM* tvi) const; diff --git a/tests/UnitTest/TreeExpansionTest.cpp b/tests/UnitTest/TreeExpansionTest.cpp new file mode 100644 index 0000000..aca99db --- /dev/null +++ b/tests/UnitTest/TreeExpansionTest.cpp @@ -0,0 +1,133 @@ +#include + +#include "TreeExpansion.h" + +namespace TreeExpansionTests +{ + TEST(SplitPath, EmptyPath) + { + EXPECT_TRUE(TreeExpansionHelper::SplitPath(L"").empty()); + } + + TEST(SplitPath, SingleKey) + { + auto keys = TreeExpansionHelper::SplitPath(L"root"); + ASSERT_EQ(keys.size(), 1u); + EXPECT_EQ(keys[0], L"root"); + } + + TEST(SplitPath, MultipleKeys) + { + auto keys = TreeExpansionHelper::SplitPath(L"root.child.[0].name"); + ASSERT_EQ(keys.size(), 4u); + EXPECT_EQ(keys[0], L"root"); + EXPECT_EQ(keys[1], L"child"); + EXPECT_EQ(keys[2], L"[0]"); + EXPECT_EQ(keys[3], L"name"); + } + + TEST(SplitPath, TrailingDotDropped) + { + auto keys = TreeExpansionHelper::SplitPath(L"root.child."); + ASSERT_EQ(keys.size(), 2u); + EXPECT_EQ(keys[1], L"child"); + } + + TEST(JoinPath, EmptyParents) + { + EXPECT_EQ(TreeExpansionHelper::JoinPath({}, L"root"), L"root"); + } + + TEST(JoinPath, Nested) + { + EXPECT_EQ(TreeExpansionHelper::JoinPath({ L"root", L"[0]" }, L"key"), L"root.[0].key"); + } + + TEST(MatchExpansion, EmptyOldState) + { + TreeExpansionState oldState; + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"a", L"b" }); + EXPECT_TRUE(toExpand.empty()); + EXPECT_TRUE(toSelect.empty()); + } + + TEST(MatchExpansion, PathStillExists) + { + TreeExpansionState oldState; + oldState.expandedPaths[L"root.child"] = true; + oldState.expandedPaths[L"root.gone"] = false; + + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"root", L"root.child", L"root.other" }); + ASSERT_EQ(toExpand.size(), 1u); + EXPECT_EQ(toExpand[0], L"root.child"); + EXPECT_TRUE(toSelect.empty()); + } + + TEST(MatchExpansion, CollapsedPathsNotReExpanded) + { + TreeExpansionState oldState; + oldState.expandedPaths[L"root.a"] = false; + oldState.expandedPaths[L"root.b"] = true; + + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"root.a", L"root.b" }); + ASSERT_EQ(toExpand.size(), 1u); + EXPECT_EQ(toExpand[0], L"root.b"); + } + + TEST(MatchExpansion, SelectionRestoredWhenExists) + { + TreeExpansionState oldState; + oldState.selectedPath = { L"root", L"child" }; + + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"root", L"root.child" }); + ASSERT_EQ(toSelect.size(), 2u); + EXPECT_EQ(toSelect[0], L"root"); + EXPECT_EQ(toSelect[1], L"child"); + } + + TEST(MatchExpansion, SelectionDroppedWhenMissing) + { + TreeExpansionState oldState; + oldState.selectedPath = { L"root", L"gone" }; + + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"root", L"root.here" }); + EXPECT_TRUE(toSelect.empty()); + } + + TEST(MatchExpansion, EmptyNewTreeRestoresNothing) + { + TreeExpansionState oldState; + oldState.expandedPaths[L"root.child"] = true; + oldState.selectedPath = { L"root", L"child" }; + + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, {}); + EXPECT_TRUE(toExpand.empty()); + EXPECT_TRUE(toSelect.empty()); + } + + TEST(MatchExpansion, NewPathsNotInOldStateAreIgnored) + { + TreeExpansionState oldState; + oldState.expandedPaths[L"root.child"] = true; + + // Nodes that appeared in the new document must stay collapsed + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"root", L"root.fresh" }); + EXPECT_TRUE(toExpand.empty()); + EXPECT_TRUE(toSelect.empty()); + } + + TEST(MatchExpansion, PreservesOrderOfNewPaths) + { + TreeExpansionState oldState; + oldState.expandedPaths[L"a"] = true; + oldState.expandedPaths[L"b"] = true; + oldState.expandedPaths[L"c"] = true; + + // Follows the order of newPaths, not the hash order of expandedPaths + auto [toExpand, toSelect] = TreeExpansionHelper::MatchExpansion(oldState, { L"c", L"a", L"b" }); + ASSERT_EQ(toExpand.size(), 3u); + EXPECT_EQ(toExpand[0], L"c"); + EXPECT_EQ(toExpand[1], L"a"); + EXPECT_EQ(toExpand[2], L"b"); + } +} // namespace TreeExpansionTests diff --git a/tests/UnitTest/UnitTest.vcxproj b/tests/UnitTest/UnitTest.vcxproj index 70446d7..e28ab0c 100644 --- a/tests/UnitTest/UnitTest.vcxproj +++ b/tests/UnitTest/UnitTest.vcxproj @@ -155,6 +155,7 @@ + @@ -163,6 +164,7 @@ + @@ -174,6 +176,7 @@ + From 88c6b9c0ce7ac840eb6d97fe51a02a08a14f8cfd Mon Sep 17 00:00:00 2001 From: leoshone Date: Thu, 3 Sep 2026 19:09:54 +0800 Subject: [PATCH 6/7] Cache the JSON tree per tab instead of drawing it on tab switch When "Follow current tab" is off (the default) the plugin never drew the tree on its own, but it also never cleared it: the tree kept showing the document of some earlier tab, with no indication that it belonged there. With this change the tree is only ever drawn when the user asks for it ("Refresh JSON Tree"). Switching tabs stores the tree of the tab being left and puts it back verbatim when the tab is activated again, so no re-parsing happens and the expansion state and selection survive. The "Follow current tab" option is kept and behaves exactly as before when enabled: the document of the activated tab is parsed immediately. Only its "off" path changes, from "do nothing" to "remember per tab". Notes: - Snapshots live in memory only and are dropped when the buffer is closed, together with the association to the current buffer. - "Auto format on open" now formats the document without drawing the tree, so opening a file still cannot trigger a parse. - Formatting now redraws the tree while preserving its expansion state, which keeps it consistent with Refresh. - Built on top of the TreeExpansion helpers introduced for Refresh. --- src/NppJsonViewer/JsonViewDlg.cpp | 245 ++++++++++++++++-- src/NppJsonViewer/JsonViewDlg.h | 27 +- src/NppJsonViewer/NPPJSONViewer.vcxproj | 2 + .../NPPJSONViewer.vcxproj.filters | 6 + src/NppJsonViewer/NppJsonPlugin.cpp | 29 ++- src/NppJsonViewer/TreeState.cpp | 48 ++++ src/NppJsonViewer/TreeState.h | 44 ++++ src/NppJsonViewer/TreeViewCtrl.cpp | 19 ++ src/NppJsonViewer/TreeViewCtrl.h | 1 + tests/UnitTest/TreeStateTest.cpp | 134 ++++++++++ tests/UnitTest/UnitTest.vcxproj | 3 + 11 files changed, 534 insertions(+), 24 deletions(-) create mode 100644 src/NppJsonViewer/TreeState.cpp create mode 100644 src/NppJsonViewer/TreeState.h create mode 100644 tests/UnitTest/TreeStateTest.cpp diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index 8d893b5..7f42bb3 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -68,14 +68,29 @@ void JsonViewDlg::ShowDlg(bool bShow) if (bShow) { - // Draw json tree now - DrawJsonTree(); + m_nCurrentBufferId = GetCurrentBufferId(); + + // Showing the panel is not a request to parse anything. When the plugin + // follows the current tab the tree is drawn as before; otherwise only a + // snapshot left behind by an explicit "Refresh JSON Tree" is restored. + if (m_pSetting->bFollowCurrentTab) + DrawJsonTree(); + else + RestoreTabState(m_nCurrentBufferId); } DockingDlgInterface::display(bShow); } void JsonViewDlg::FormatJson() +{ + // After formatting, the tree is redrawn and the tab snapshot updated. + // The expansion state of the previous tree is preserved. + if (FormatJsonDocument()) + ReDrawJsonTree(true, true); +} + +auto JsonViewDlg::FormatJsonDocument() -> bool { UpdateTitle(); @@ -86,7 +101,7 @@ void JsonViewDlg::FormatJson() { const std::wstring msg = IsMultiSelection(selectedData) ? JSON_ERR_MULTI_SELECTION : JSON_ERR_PARSE; ShowMessage(JSON_INFO_TITLE, msg, MB_OK | MB_ICONINFORMATION); - return; + return false; } auto [le, lf, indentChar, indentLen] = GetFormatSetting(); @@ -101,12 +116,12 @@ void JsonViewDlg::FormatJson() else { if (CheckForTokenUndefined(JsonViewDlg::eMethod::FormatJson, selectedText.value(), res, NULL)) - return; + return false; ReportError(res); } - ReDrawJsonTree(); + return true; } void JsonViewDlg::CompressJson() @@ -280,25 +295,77 @@ void JsonViewDlg::ProcessScintillaData(const ScintillaData& scintillaData, std:: scintillaData); } -void JsonViewDlg::HandleTabActivated() +void JsonViewDlg::HandleTabActivated(uptr_t activatedBufferId) { const bool bIsVisible = isCreated() && isVisible(); - if (bIsVisible) + if (!bIsVisible) { - m_pEditor->RefreshViewHandle(); - if (m_pEditor->IsJsonFile()) + // The panel is hidden: nothing is drawn, but the buffer id has to follow + // along so that the tree is attached to the right tab once it is shown. + m_nCurrentBufferId = activatedBufferId; + return; + } + + // Remember the tree of the tab we are leaving (only when one was drawn) + if (!m_pSetting->bFollowCurrentTab) + CaptureCurrentTabState(); + + m_pEditor->RefreshViewHandle(); + m_nCurrentBufferId = activatedBufferId; + + if (m_pEditor->IsJsonFile()) + { + if (m_pSetting->bFollowCurrentTab) { - if (m_pSetting->bFollowCurrentTab) - { - DrawJsonTree(); - } + // Original behaviour: parse the document of the newly activated tab + DrawJsonTree(); if (m_pSetting->bAutoFormat) - { FormatJson(); - } + } + else + { + // Otherwise the tab is never parsed on its own. Put back the + // snapshot recorded for it, or leave the tree empty when the user + // has not refreshed it yet. + RestoreTabState(activatedBufferId); } } + else + { + RestoreTabState(activatedBufferId); + } + + UpdateTitle(); +} + +void JsonViewDlg::HandleFileClosed(uptr_t bufferId) +{ + m_tabSnapshots.erase(bufferId); + + // Notepad++ does not guarantee whether NPPN_FILECLOSED or + // NPPN_BUFFERACTIVATED arrives first. Forgetting the association here + // prevents a later CaptureCurrentTabState() from re-creating the snapshot + // of the buffer that has just been closed. + if (bufferId == m_nCurrentBufferId) + m_nCurrentBufferId = 0; +} + +void JsonViewDlg::HandleFileOpened() +{ + // "Auto format on open" still applies, but formatting a document is not a + // request to draw its tree: the user decides when to refresh it. + if (m_pSetting->bAutoFormat && isCreated() && isVisible() && !m_pSetting->bFollowCurrentTab) + { + m_pEditor->RefreshViewHandle(); + if (m_pEditor->IsJsonFile()) + FormatJsonDocument(); + } +} + +void JsonViewDlg::SyncBufferId() +{ + m_nCurrentBufferId = GetCurrentBufferId(); } void JsonViewDlg::ValidateJson() @@ -394,17 +461,20 @@ void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion) if (bPreserveExpansion && bHasCurrentTree && m_pTreeView->GetNodeCount() > 1) ApplyExpansionState(expState); + // Update the snapshot of the current tab with the freshly drawn tree + SaveTreeSnapshot(); + // Enable all buttons and treeView EnableControls(ctrls, true); } -void JsonViewDlg::ReDrawJsonTree(bool bForce) +void JsonViewDlg::ReDrawJsonTree(bool bForce, bool bPreserveExpansion) { const bool bIsVisible = isCreated() && isVisible(); const bool bReDraw = bForce || bIsVisible; if (bReDraw) { - DrawJsonTree(); + DrawJsonTree(bPreserveExpansion); } } @@ -736,6 +806,144 @@ void JsonViewDlg::SelectByPath(const std::vector& path) } } +void JsonViewDlg::CaptureCurrentTabState() +{ + // m_nCurrentBufferId == 0 means "unknown" (for instance the buffer that was + // displayed has just been closed). Nothing can be attached in that case. + if (m_nCurrentBufferId == 0) + return; + + // Snapshot the tree only when it holds a real drawn tree of this tab. + // The empty placeholder tree (single root) is not worth capturing. + if (!m_pTreeView->GetRoot()) + return; + + if (m_pTreeView->GetNodeCount() <= 1) + return; + + m_tabSnapshots[m_nCurrentBufferId] = CaptureTreeState(); +} + +void JsonViewDlg::RestoreTabState(uptr_t bufferId) +{ + auto find = m_tabSnapshots.find(bufferId); + if (find == m_tabSnapshots.end() || find->second.roots.empty()) + { + ShowEmptyTree(); + return; + } + + ApplyTreeState(find->second); +} + +void JsonViewDlg::ShowEmptyTree() +{ + m_pTreeView->InitTree(); + m_pTreeView->Expand(m_pTreeView->GetRoot()); +} + +auto JsonViewDlg::CaptureTreeState() const -> TreeState +{ + TreeState state; + + auto hRoot = m_pTreeView->GetRoot(); + if (!hRoot) + return state; + + // The tree root ("JSON") itself is not part of the snapshot: it is always + // recreated by InitTree(). Only its children are captured. + std::function&)> captureChildren; + captureChildren = [&](HTREEITEM hParent, std::vector& siblings) { + for (HTREEITEM hChild = m_pTreeView->GetChildItem(hParent); hChild; + hChild = m_pTreeView->GetNextSibling(hChild)) + { + TreeStateNode node; + node.text = m_pTreeView->GetNodeName(hChild, false); + + auto pPosition = m_pTreeView->GetNodePosition(hChild); + if (pPosition) + node.pos = *pPosition; + + node.expanded = m_pTreeView->IsExpanded(hChild); + + captureChildren(hChild, node.children); + + siblings.push_back(std::move(node)); + } + }; + + captureChildren(hRoot, state.roots); + + // Selection path (keys from the root down to the selected node) + state.selectedPath = GetCurrentSelectedPath(); + + return state; +} + +void JsonViewDlg::ApplyTreeState(const TreeState& state) +{ + // Rebuild the tree control without intermediate redraws + HWND hTree = m_pTreeView->GetTreeViewHandle(); + ::SendMessage(hTree, WM_SETREDRAW, FALSE, 0); + + m_pTreeView->InitTree(); + auto hRoot = m_pTreeView->GetRoot(); + + std::function insertNode; + insertNode = [&](const TreeStateNode& node, HTREEITEM hParent, HTREEITEM hAfter) -> HTREEITEM { + LPARAM lparam = 0; + if (node.pos.has_value()) + lparam = reinterpret_cast(new Position(node.pos.value())); + + auto hInserted = m_pTreeView->InsertNodeAfter(hAfter, node.text, lparam, hParent); + + HTREEITEM hPrev = nullptr; + for (const auto& child : node.children) + { + hPrev = insertNode(child, hInserted, hPrev); + } + + if (node.expanded && !node.children.empty()) + m_pTreeView->Expand(hInserted); + + return hInserted; + }; + + HTREEITEM hPrev = nullptr; + for (const auto& rootChild : state.roots) + { + hPrev = insertNode(rootChild, hRoot, hPrev); + } + + // Restore selection. A programmatic selection reports TVC_UNKNOWN in + // TVN_SELCHANGED, so it will not make the editor jump to the node. + auto hSelected = FindNodeByPath(state.selectedPath); + if (hSelected) + m_pTreeView->SetSelection(hSelected); + + // The root ("JSON") is always expanded + m_pTreeView->Expand(hRoot); + + ::SendMessage(hTree, WM_SETREDRAW, TRUE, 0); + ::InvalidateRect(hTree, nullptr, TRUE); +} + +void JsonViewDlg::SaveTreeSnapshot() +{ + if (m_nCurrentBufferId == 0) + return; + + if (m_pTreeView->GetNodeCount() > 1) + m_tabSnapshots[m_nCurrentBufferId] = CaptureTreeState(); + else + m_tabSnapshots.erase(m_nCurrentBufferId); +} + +uptr_t JsonViewDlg::GetCurrentBufferId() const +{ + return static_cast(::SendMessage(_hParent, NPPM_GETCURRENTBUFFERID, 0, 0)); +} + void JsonViewDlg::UpdateTitle() { const auto titleFileName = GetTitleFileName(); @@ -990,6 +1198,9 @@ void JsonViewDlg::ContextMenuExpand(bool bExpand) bExpand ? m_pTreeView->Expand(htiNext) : m_pTreeView->Collapse(htiNext); htiNext = m_pTreeView->NextItem(htiNext, htiSelected); } + + // Keep the snapshot of this tab in sync with the new expansion state + SaveTreeSnapshot(); } auto JsonViewDlg::CopyName() const -> std::wstring diff --git a/src/NppJsonViewer/JsonViewDlg.h b/src/NppJsonViewer/JsonViewDlg.h index d33133b..9660c0c 100644 --- a/src/NppJsonViewer/JsonViewDlg.h +++ b/src/NppJsonViewer/JsonViewDlg.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -15,6 +16,7 @@ #include "JsonNode.h" #include "TreeHandler.h" #include "TreeExpansion.h" +#include "TreeState.h" class JsonViewDlg @@ -44,9 +46,13 @@ class JsonViewDlg void ShowDlg(bool bShow); void FormatJson(); + auto FormatJsonDocument() -> bool; // true = document handled, caller may redraw the tree void CompressJson(); void SortJsonByKey(); - void HandleTabActivated(); + void HandleTabActivated(uptr_t activatedBufferId); + void HandleFileClosed(uptr_t bufferId); + void HandleFileOpened(); + void SyncBufferId(); void UpdateTitle(); HTREEITEM InsertToTree(HTREEITEM parent, const std::string& text) override; @@ -55,7 +61,7 @@ class JsonViewDlg private: void DrawJsonTree(bool bPreserveExpansion = false); - void ReDrawJsonTree(bool bForce = false); + void ReDrawJsonTree(bool bForce = false, bool bPreserveExpansion = false); void HighlightAsJson(bool bForcefully = false) const; auto PopulateTreeUsingSax(HTREEITEM tree_root, const std::string& jsonText) -> std::optional; @@ -85,6 +91,19 @@ class JsonViewDlg // Returns nullptr when any level of the path cannot be found. auto FindNodeByPath(const std::vector& path) const -> HTREEITEM; + // Per-tab snapshots. The tree control is a single shared window, so leaving + // a tab means capturing what it looked like; coming back replays the + // snapshot instead of parsing the document again. + void CaptureCurrentTabState(); + void RestoreTabState(uptr_t bufferId); + void ShowEmptyTree(); + + auto CaptureTreeState() const -> TreeState; + void ApplyTreeState(const TreeState& state); + void SaveTreeSnapshot(); + + auto GetCurrentBufferId() const -> uptr_t; + auto GetTitleFileName() const -> std::wstring; void PrepareButtons(); void SetIconAndTooltip(eButton ctrlType, const std::wstring& toolTip); @@ -147,4 +166,8 @@ class JsonViewDlg std::unique_ptr m_pTreeView = nullptr; std::unique_ptr m_pTreeViewZoom = nullptr; std::shared_ptr m_pSetting = nullptr; + + // Per-tab (buffer) tree snapshots: buffer id -> captured tree state + std::unordered_map m_tabSnapshots; + uptr_t m_nCurrentBufferId = 0; }; diff --git a/src/NppJsonViewer/NPPJSONViewer.vcxproj b/src/NppJsonViewer/NPPJSONViewer.vcxproj index 1015296..618ae82 100644 --- a/src/NppJsonViewer/NPPJSONViewer.vcxproj +++ b/src/NppJsonViewer/NPPJSONViewer.vcxproj @@ -198,6 +198,7 @@ + @@ -219,6 +220,7 @@ + diff --git a/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters b/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters index a4b6946..efaec38 100644 --- a/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters +++ b/src/NppJsonViewer/NPPJSONViewer.vcxproj.filters @@ -60,6 +60,9 @@ Source Files + + Source Files + Source Files @@ -116,6 +119,9 @@ Header Files + + Header Files + Header Files diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index 40e96c0..41e7332 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -64,7 +64,25 @@ void NppJsonPlugin::ProcessNotification(const SCNotification* notifyCode) { if (m_pJsonViewDlg && m_bNppReady && !m_bAboutToClose) { - m_pJsonViewDlg->HandleTabActivated(); + m_pJsonViewDlg->HandleTabActivated(notifyCode->nmhdr.idFrom); + } + break; + } + + case NPPN_FILECLOSED: + { + if (m_pJsonViewDlg) + { + m_pJsonViewDlg->HandleFileClosed(notifyCode->nmhdr.idFrom); + } + break; + } + + case NPPN_FILEOPENED: + { + if (m_pJsonViewDlg && m_bNppReady && !m_bAboutToClose) + { + m_pJsonViewDlg->HandleFileOpened(); } break; } @@ -77,11 +95,12 @@ void NppJsonPlugin::ProcessNotification(const SCNotification* notifyCode) case NPPN_READY: { - // This is workaround where dialog does not show tree on launch - if (m_pJsonViewDlg && m_pJsonViewDlg->isVisible() && !m_bAboutToClose) + // The tree is never drawn automatically: every tab starts empty and the + // user decides when to refresh it. Only the current buffer id is picked + // up so that the first refresh is attached to the right tab. + if (m_pJsonViewDlg && !m_bAboutToClose) { - ::SendMessage(m_pJsonViewDlg->getHSelf(), WM_COMMAND, IDC_BTN_REFRESH, 0); - m_pJsonViewDlg->UpdateTitle(); + m_pJsonViewDlg->SyncBufferId(); } m_bNppReady = true; break; diff --git a/src/NppJsonViewer/TreeState.cpp b/src/NppJsonViewer/TreeState.cpp new file mode 100644 index 0000000..a33b99c --- /dev/null +++ b/src/NppJsonViewer/TreeState.cpp @@ -0,0 +1,48 @@ +#include "TreeState.h" + +namespace +{ + auto AreNodesEqual(const TreeStateNode& lhs, const TreeStateNode& rhs) -> bool + { + if (lhs.text != rhs.text || lhs.expanded != rhs.expanded) + return false; + + if (lhs.pos.has_value() != rhs.pos.has_value()) + return false; + + if (lhs.pos.has_value()) + { + if (lhs.pos->nLine != rhs.pos->nLine || lhs.pos->nColumn != rhs.pos->nColumn + || lhs.pos->nKeyLength != rhs.pos->nKeyLength) + return false; + } + + if (lhs.children.size() != rhs.children.size()) + return false; + + for (size_t i = 0; i < lhs.children.size(); ++i) + { + if (!AreNodesEqual(lhs.children[i], rhs.children[i])) + return false; + } + + return true; + } +} + +auto TreeStateHelper::AreEqual(const TreeState& lhs, const TreeState& rhs) -> bool +{ + if (lhs.selectedPath != rhs.selectedPath) + return false; + + if (lhs.roots.size() != rhs.roots.size()) + return false; + + for (size_t i = 0; i < lhs.roots.size(); ++i) + { + if (!AreNodesEqual(lhs.roots[i], rhs.roots[i])) + return false; + } + + return true; +} diff --git a/src/NppJsonViewer/TreeState.h b/src/NppJsonViewer/TreeState.h new file mode 100644 index 0000000..c0257f2 --- /dev/null +++ b/src/NppJsonViewer/TreeState.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "JsonNode.h" + +/* + * TreeState is a per-tab snapshot of the JSON tree: the nodes themselves + * (text and editor position), their expansion state and the current selection. + * It is a plain in-memory model, independent from any Win32 control. + * + * It is used to remember what a tab looked like: when the user switches away + * from a tab and later comes back, the tree is restored from the snapshot + * instead of being parsed again. See TreeExpansion.h for the lighter-weight + * "same document, tree rebuilt" case (refresh). + * + * Only the children of the tree root ("JSON") are captured; the root itself is + * always recreated by TreeViewCtrl::InitTree(). + */ +struct TreeStateNode +{ + std::wstring text; // Display text of the node (including trailing [n]/{n} counts) + std::optional pos; // Editor position of the key (nullopt when the node has none) + bool expanded = false; + std::vector children; +}; + +struct TreeState +{ + std::vector roots; // Children of the tree root ("JSON") + std::vector selectedPath; // Path of the selected node (key per level), empty when none +}; + +class TreeStateHelper +{ +public: + /* + * Recursive comparison used by unit tests: verifies that two states have + * identical structure, texts, positions, expansion flags and selection. + */ + static auto AreEqual(const TreeState& lhs, const TreeState& rhs) -> bool; +}; diff --git a/src/NppJsonViewer/TreeViewCtrl.cpp b/src/NppJsonViewer/TreeViewCtrl.cpp index 40f1a0b..debf4e1 100644 --- a/src/NppJsonViewer/TreeViewCtrl.cpp +++ b/src/NppJsonViewer/TreeViewCtrl.cpp @@ -49,6 +49,25 @@ auto TreeViewCtrl::InsertNode(const std::wstring& text, LPARAM lparam, HTREEITEM return item; } +// Inserts a node right after hAfter (or as the first child when hAfter is null). +// Needed to rebuild a tree in a specific sibling order when restoring a snapshot. +auto TreeViewCtrl::InsertNodeAfter(HTREEITEM hAfter, const std::wstring& text, LPARAM lparam, HTREEITEM parentNode) -> HTREEITEM +{ + TV_INSERTSTRUCT tvInsert {}; + + tvInsert.hParent = (parentNode == TVI_ROOT) ? NULL : parentNode; + tvInsert.hInsertAfter = hAfter ? hAfter : TVI_FIRST; + + if (text.length() + 1 > m_nMaxNodeTextLength) + m_nMaxNodeTextLength = text.length() + 1; + + tvInsert.item.mask = TVIF_HANDLE | TVIF_TEXT | TVIF_PARAM; + tvInsert.item.pszText = const_cast(text.c_str()); + tvInsert.item.lParam = lparam; + + return reinterpret_cast(SendDlgItemMessage(m_hParent, m_nCtrlID, TVM_INSERTITEM, 0, reinterpret_cast(&tvInsert))); +} + auto TreeViewCtrl::GetChildItem(HTREEITEM node) const -> HTREEITEM { return TreeView_GetNextItem(m_hTree, node, TVGN_CHILD); diff --git a/src/NppJsonViewer/TreeViewCtrl.h b/src/NppJsonViewer/TreeViewCtrl.h index 9bb9edd..34d6e44 100644 --- a/src/NppJsonViewer/TreeViewCtrl.h +++ b/src/NppJsonViewer/TreeViewCtrl.h @@ -26,6 +26,7 @@ class TreeViewCtrl auto InitTree() -> HTREEITEM; auto InsertNode(const std::wstring& text, LPARAM lparam, HTREEITEM parentNode) -> HTREEITEM; + auto InsertNodeAfter(HTREEITEM hAfter, const std::wstring& text, LPARAM lparam, HTREEITEM parentNode) -> HTREEITEM; void UpdateNodeText(HTREEITEM node, const std::wstring& text); auto GetNodeCount() const -> unsigned int; diff --git a/tests/UnitTest/TreeStateTest.cpp b/tests/UnitTest/TreeStateTest.cpp new file mode 100644 index 0000000..b2a74ac --- /dev/null +++ b/tests/UnitTest/TreeStateTest.cpp @@ -0,0 +1,134 @@ +#include + +#include "TreeState.h" + +namespace TreeStateTests +{ + TEST(AreEqual, EmptyStates) + { + TreeState a, b; + EXPECT_TRUE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, IdenticalStates) + { + TreeStateNode child; + child.text = L"a : 1"; + + TreeStateNode parent; + parent.text = L"obj {1}"; + parent.expanded = true; + parent.children = {child}; + + TreeState a, b; + a.roots.push_back(parent); + b.roots.push_back(parent); + a.selectedPath = {L"obj", L"a"}; + b.selectedPath = {L"obj", L"a"}; + + EXPECT_TRUE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, DifferentText) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + a.roots.push_back(n); + n.text = L"other"; + b.roots.push_back(n); + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, DifferentExpansion) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + n.expanded = true; + a.roots.push_back(n); + n.expanded = false; + b.roots.push_back(n); + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, DifferentSelection) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + a.roots.push_back(n); + b.roots.push_back(n); + a.selectedPath = {L"key"}; + b.selectedPath = {L"other"}; + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, DifferentRootCount) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + a.roots.push_back(n); + b.roots.push_back(n); + b.roots.push_back(n); + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, DifferentChildrenCount) + { + TreeState a, b; + TreeStateNode parent; + parent.text = L"obj {2}"; + TreeStateNode c1, c2; + c1.text = L"a"; + c2.text = L"b"; + parent.children = {c1, c2}; + a.roots.push_back(parent); + + parent.children = {c1}; + b.roots.push_back(parent); + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, PositionCompared) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + n.pos = Position{3, 5, 4}; + a.roots.push_back(n); + + TreeStateNode m; + m.text = L"key"; + m.pos = Position{3, 5, 4}; + b.roots.push_back(m); + + EXPECT_TRUE(TreeStateHelper::AreEqual(a, b)); + + m.pos = Position{4, 5, 4}; + b.roots.clear(); + b.roots.push_back(m); + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } + + TEST(AreEqual, PositionPresenceCompared) + { + TreeState a, b; + TreeStateNode n; + n.text = L"key"; + n.pos = Position{1, 2, 3}; + a.roots.push_back(n); + + TreeStateNode m; + m.text = L"key"; // no position at all + b.roots.push_back(m); + + EXPECT_FALSE(TreeStateHelper::AreEqual(a, b)); + } +} diff --git a/tests/UnitTest/UnitTest.vcxproj b/tests/UnitTest/UnitTest.vcxproj index e28ab0c..b0a303e 100644 --- a/tests/UnitTest/UnitTest.vcxproj +++ b/tests/UnitTest/UnitTest.vcxproj @@ -156,6 +156,7 @@ + @@ -165,6 +166,7 @@ + @@ -177,6 +179,7 @@ + From cdda86c0516d62a807cb740fe898ae05bb0c0f70 Mon Sep 17 00:00:00 2001 From: leoshone Date: Thu, 3 Sep 2026 19:36:15 +0800 Subject: [PATCH 7/7] Draw the tree once when a json file is opened (DRAW_ON_OPEN) Adds an option, off by default, that draws the tree of a json document as soon as the file is opened. It complements the per-tab snapshot caching: the document is parsed exactly once, and switching back to the tab afterwards replays the stored snapshot instead of parsing again. The check lives in RestoreTabState(), the single place reached when the tree of a tab has never been drawn, so opening a file, switching back to a tab and showing the panel are all covered by one code path. Drawing on open is initiated by the plugin, not by the user, so parse errors are reported as a node inside the tree rather than through a modal dialog: DrawJsonTree() takes a bSilent flag for that. The tree is drawn for documents whose language is JSON, the same criterion the existing "follow current tab" uses. --- src/NppJsonViewer/Define.h | 2 ++ src/NppJsonViewer/JsonViewDlg.cpp | 32 ++++++++++++++++++++++++++--- src/NppJsonViewer/JsonViewDlg.h | 7 ++++++- src/NppJsonViewer/NppJsonPlugin.cpp | 3 +++ src/NppJsonViewer/Profile.cpp | 4 ++++ src/NppJsonViewer/SettingsDlg.cpp | 2 ++ src/NppJsonViewer/resource.h | 1 + src/NppJsonViewer/resource.rc | 2 ++ tests/UnitTest/ProfileTest.cpp | 18 ++++++++++++++++ 9 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/NppJsonViewer/Define.h b/src/NppJsonViewer/Define.h index 0e2a365..b734b4b 100644 --- a/src/NppJsonViewer/Define.h +++ b/src/NppJsonViewer/Define.h @@ -68,6 +68,7 @@ const TCHAR STR_INI_FORMATTING_INDENTCOUNT[] = TEXT("INDENTATION_COUNT"); const TCHAR STR_INI_OTHER_SEC[] = TEXT("Others"); const TCHAR STR_INI_OTHER_FOLLOW_TAB[] = TEXT("FOLLOW_TAB"); const TCHAR STR_INI_OTHER_TREE_ZOOM[] = TEXT("TREE_ZOOM_LEVEL"); +const TCHAR STR_INI_OTHER_DRAW_ON_OPEN[] = TEXT("DRAW_ON_OPEN"); const TCHAR STR_INI_OTHER_AUTO_FORMAT[] = TEXT("AUTO_FORMAT"); const TCHAR STR_INI_OTHER_USE_HIGHLIGHT[] = TEXT("USE_JSON_HIGHLIGHT"); const TCHAR STR_INI_OTHER_IGNORE_COMMENT[] = TEXT("IGNORE_COMMENT"); @@ -118,6 +119,7 @@ struct Setting LineFormat lineFormat = LineFormat::DEFAULT; Indent indent {}; bool bFollowCurrentTab = false; + bool bDrawOnOpen = false; // Draw the tree once when a json file is opened bool bAutoFormat = false; bool bUseJsonHighlight = true; int nTreeZoom = 100; // Tree view font zoom in percent (80..250) diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index 7f42bb3..4d90c2a 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -368,6 +368,17 @@ void JsonViewDlg::SyncBufferId() m_nCurrentBufferId = GetCurrentBufferId(); } +void JsonViewDlg::RestoreCurrentTabTree() +{ + // A tab restored from a previous session never sees NPPN_BUFFERACTIVATED, + // so the "draw tree on open" path has to be reachable from NPPN_READY too. + if (m_nCurrentBufferId == 0) + return; + + if (isCreated() && isVisible()) + RestoreTabState(m_nCurrentBufferId); +} + void JsonViewDlg::ValidateJson() { UpdateTitle(); @@ -402,7 +413,7 @@ void JsonViewDlg::ValidateJson() DrawJsonTree(); } -void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion) +void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion, bool bSilent) { UpdateTitle(); @@ -429,7 +440,7 @@ void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion) { m_pTreeView->InsertNode(JSON_ERR_PARSE, NULL, rootNode); - if (IsMultiSelection(selectedData)) + if (IsMultiSelection(selectedData) && !bSilent) { ShowMessage(JSON_INFO_TITLE, JSON_ERR_MULTI_SELECTION, MB_OK | MB_ICONINFORMATION); } @@ -443,7 +454,7 @@ void JsonViewDlg::DrawJsonTree(bool bPreserveExpansion) // Later on second launch, don't show the error message as this could be some text file // If it is real json file but has some error, then there must be more than 1 node exist. - if (!m_IsNppReady && m_pTreeView->GetNodeCount() <= 1) + if (bSilent || (!m_IsNppReady && m_pTreeView->GetNodeCount() <= 1)) { m_pTreeView->InsertNode(JSON_ERR_VALIDATE, NULL, rootNode); } @@ -829,6 +840,21 @@ void JsonViewDlg::RestoreTabState(uptr_t bufferId) auto find = m_tabSnapshots.find(bufferId); if (find == m_tabSnapshots.end() || find->second.roots.empty()) { + // Nothing has ever been drawn for this tab. With "draw tree on open" + // the tree of a json document is drawn once, here and now; from then + // on the snapshot exists and switching back never parses again. + if (m_pSetting->bDrawOnOpen) + { + m_pEditor->RefreshViewHandle(); + if (m_pEditor->IsJsonFile()) + { + // Drawn on the plugin's own initiative: never interrupt the + // user with a modal dialog, report the error in the tree only. + DrawJsonTree(false, true); // stores the snapshot on its way out + return; + } + } + ShowEmptyTree(); return; } diff --git a/src/NppJsonViewer/JsonViewDlg.h b/src/NppJsonViewer/JsonViewDlg.h index 9660c0c..67baffd 100644 --- a/src/NppJsonViewer/JsonViewDlg.h +++ b/src/NppJsonViewer/JsonViewDlg.h @@ -53,6 +53,7 @@ class JsonViewDlg void HandleFileClosed(uptr_t bufferId); void HandleFileOpened(); void SyncBufferId(); + void RestoreCurrentTabTree(); void UpdateTitle(); HTREEITEM InsertToTree(HTREEITEM parent, const std::string& text) override; @@ -60,7 +61,11 @@ class JsonViewDlg void AppendNodeCount(HTREEITEM node, unsigned elementCount, bool bArray) override; private: - void DrawJsonTree(bool bPreserveExpansion = false); + // bSilent suppresses the modal error box reported for an unparsable + // document; the error is only shown as a node inside the tree. It is used + // when the tree is drawn on its own (opening a file), where a modal dialog + // would interrupt the user who never asked for it. + void DrawJsonTree(bool bPreserveExpansion = false, bool bSilent = false); void ReDrawJsonTree(bool bForce = false, bool bPreserveExpansion = false); void HighlightAsJson(bool bForcefully = false) const; auto PopulateTreeUsingSax(HTREEITEM tree_root, const std::string& jsonText) -> std::optional; diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index 41e7332..ef36e0f 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -101,6 +101,9 @@ void NppJsonPlugin::ProcessNotification(const SCNotification* notifyCode) if (m_pJsonViewDlg && !m_bAboutToClose) { m_pJsonViewDlg->SyncBufferId(); + + if (m_pJsonViewDlg->isVisible()) + m_pJsonViewDlg->RestoreCurrentTabTree(); } m_bNppReady = true; break; diff --git a/src/NppJsonViewer/Profile.cpp b/src/NppJsonViewer/Profile.cpp index 1737621..74d0ff6 100644 --- a/src/NppJsonViewer/Profile.cpp +++ b/src/NppJsonViewer/Profile.cpp @@ -100,6 +100,9 @@ bool ProfileSetting::GetSettings(Setting& info) const bRetVal = bRetVal && ReadValue(STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM, nVal, info.nTreeZoom); if (bRetVal) info.nTreeZoom = nVal; + bRetVal = bRetVal && ReadValue(STR_INI_OTHER_SEC, STR_INI_OTHER_DRAW_ON_OPEN, nVal, info.bDrawOnOpen); + if (bRetVal) + info.bDrawOnOpen = static_cast(nVal); bRetVal = bRetVal && ReadValue(STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT, nVal, info.bAutoFormat); if (bRetVal) @@ -145,6 +148,7 @@ bool ProfileSetting::SetSettings(const Setting& info) const writeIfChanged(current.indent.len, info.indent.len, STR_INI_FORMATTING_SEC, STR_INI_FORMATTING_INDENTCOUNT); writeIfChanged(current.bFollowCurrentTab, info.bFollowCurrentTab, STR_INI_OTHER_SEC, STR_INI_OTHER_FOLLOW_TAB); writeIfChanged(current.nTreeZoom, info.nTreeZoom, STR_INI_OTHER_SEC, STR_INI_OTHER_TREE_ZOOM); + writeIfChanged(current.bDrawOnOpen, info.bDrawOnOpen, STR_INI_OTHER_SEC, STR_INI_OTHER_DRAW_ON_OPEN); writeIfChanged(current.bAutoFormat, info.bAutoFormat, STR_INI_OTHER_SEC, STR_INI_OTHER_AUTO_FORMAT); writeIfChanged(current.bUseJsonHighlight, info.bUseJsonHighlight, STR_INI_OTHER_SEC, STR_INI_OTHER_USE_HIGHLIGHT); writeIfChanged(current.parseOptions.bIgnoreComment, info.parseOptions.bIgnoreComment, STR_INI_OTHER_SEC, STR_INI_OTHER_IGNORE_COMMENT); diff --git a/src/NppJsonViewer/SettingsDlg.cpp b/src/NppJsonViewer/SettingsDlg.cpp index 5ff738f..cffeed5 100644 --- a/src/NppJsonViewer/SettingsDlg.cpp +++ b/src/NppJsonViewer/SettingsDlg.cpp @@ -123,6 +123,7 @@ bool SettingsDlg::Apply() m_pSetting->lineFormat = LineFormat::SINGLELINE; m_pSetting->bFollowCurrentTab = CUtility::GetCheckboxStatus(::GetDlgItem(_hSelf, IDC_CHK_FOLLOW_CURRENT_DOC)); + m_pSetting->bDrawOnOpen = CUtility::GetCheckboxStatus(::GetDlgItem(_hSelf, IDC_CHK_DRAW_ON_OPEN)); m_pSetting->bAutoFormat = CUtility::GetCheckboxStatus(::GetDlgItem(_hSelf, IDC_CHK_FORMAT_ON_OPEN)); m_pSetting->bUseJsonHighlight = CUtility::GetCheckboxStatus(::GetDlgItem(_hSelf, IDC_CHK_JSON_HIGHLIGHT)); m_pSetting->parseOptions.bIgnoreTrailingComma = CUtility::GetCheckboxStatus(::GetDlgItem(_hSelf, IDC_CHK_IGNORE_COMMA)); @@ -220,6 +221,7 @@ void SettingsDlg::SyncUIControlsWithSettings() // Set all checkbox controls setCheckboxIfValid(IDC_CHK_FOLLOW_CURRENT_DOC, m_pSetting->bFollowCurrentTab); + setCheckboxIfValid(IDC_CHK_DRAW_ON_OPEN, m_pSetting->bDrawOnOpen); setCheckboxIfValid(IDC_CHK_FORMAT_ON_OPEN, m_pSetting->bAutoFormat); setCheckboxIfValid(IDC_CHK_JSON_HIGHLIGHT, m_pSetting->bUseJsonHighlight); setCheckboxIfValid(IDC_CHK_IGNORE_COMMA, m_pSetting->parseOptions.bIgnoreTrailingComma); diff --git a/src/NppJsonViewer/resource.h b/src/NppJsonViewer/resource.h index 13d1e52..038817e 100644 --- a/src/NppJsonViewer/resource.h +++ b/src/NppJsonViewer/resource.h @@ -44,6 +44,7 @@ #define IDC_CHK_REPLACE_UNDEFINED 1033 #define IDC_ZOOM_SLIDER 1034 #define IDC_ZOOM_PERCENT 1035 +#define IDC_CHK_DRAW_ON_OPEN 1036 #define IDM_COPY_TREEITEM 40001 #define IDM_COPY_NODENAME 40002 #define IDM_COPY_NODEVALUE 40003 diff --git a/src/NppJsonViewer/resource.rc b/src/NppJsonViewer/resource.rc index 2c10a78..f257ef7 100644 --- a/src/NppJsonViewer/resource.rc +++ b/src/NppJsonViewer/resource.rc @@ -109,6 +109,8 @@ BEGIN CONTROL "Use json highlighting",IDC_CHK_JSON_HIGHLIGHT,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,59,140,10 CONTROL "Replace value 'undefined' with 'null'",IDC_CHK_REPLACE_UNDEFINED, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,72,140,10 + CONTROL "Draw tree when a json file is opened",IDC_CHK_DRAW_ON_OPEN, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,85,140,10 GROUPBOX " Indentation: ",IDC_STATIC,153,7,140,43 CONTROL "Auto detect",IDC_RADIO_INDENT_AUTO,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,160,21,50,10 CONTROL "Use tab",IDC_RADIO_INDENT_TAB,"Button",BS_AUTORADIOBUTTON,240,21,50,10 diff --git a/tests/UnitTest/ProfileTest.cpp b/tests/UnitTest/ProfileTest.cpp index 873d9f6..5ab948e 100644 --- a/tests/UnitTest/ProfileTest.cpp +++ b/tests/UnitTest/ProfileTest.cpp @@ -135,6 +135,7 @@ namespace ProfileSettingTests EXPECT_EQ(setting.indent.style, IndentStyle::AUTO); EXPECT_EQ(setting.bFollowCurrentTab, false); + EXPECT_EQ(setting.bDrawOnOpen, false); EXPECT_EQ(setting.bAutoFormat, false); EXPECT_EQ(setting.bUseJsonHighlight, true); EXPECT_EQ(setting.nTreeZoom, 100); @@ -154,6 +155,7 @@ namespace ProfileSettingTests expected.bAutoFormat = true; expected.bFollowCurrentTab = true; + expected.bDrawOnOpen = true; expected.bAutoFormat = true; expected.bUseJsonHighlight = false; @@ -172,6 +174,7 @@ namespace ProfileSettingTests EXPECT_EQ(actual.indent.style, expected.indent.style); EXPECT_EQ(actual.bFollowCurrentTab, expected.bFollowCurrentTab); + EXPECT_EQ(actual.bDrawOnOpen, expected.bDrawOnOpen); EXPECT_EQ(actual.bAutoFormat, expected.bAutoFormat); EXPECT_EQ(actual.bUseJsonHighlight, expected.bUseJsonHighlight); EXPECT_EQ(actual.nTreeZoom, expected.nTreeZoom); @@ -202,4 +205,19 @@ namespace ProfileSettingTests EXPECT_EQ(actual.nTreeZoom, zoom); } } + + TEST_F(ProfileTest, DrawOnOpen_RoundTrip) + { + Setting expected, actual; + + expected.bDrawOnOpen = true; + EXPECT_TRUE(m_pProfile->SetSettings(expected)); + EXPECT_TRUE(m_pProfile->GetSettings(actual)); + EXPECT_EQ(actual.bDrawOnOpen, true); + + expected.bDrawOnOpen = false; + EXPECT_TRUE(m_pProfile->SetSettings(expected)); + EXPECT_TRUE(m_pProfile->GetSettings(actual)); + EXPECT_EQ(actual.bDrawOnOpen, false); + } } // namespace ProfileSettingTests