Skip to content

Commit 956a059

Browse files
committed
simplify IInheritable macros and comments
1 parent 9ace61a commit 956a059

8 files changed

Lines changed: 391 additions & 500 deletions

File tree

src/cascadia/TerminalSettingsModel/AppearanceConfig.cpp

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,8 @@ void AppearanceConfig::LayerJson(const Json::Value& json)
242242
_logSettingIfSet(OpacityKey, HasOpacity());
243243

244244
// ColorScheme: translate the polymorphic on-disk "colorScheme" key (string, or
245-
// { "dark", "light" }) into the two independent internal keys. A string sets both sides;
246-
// an object sets only the side(s) present (the missing side is left untouched so it
247-
// inherits / retains a prior layer's value). Matches main's LayerJson behavior.
248-
// ColorScheme: translate the polymorphic on-disk "colorScheme" key (string, or
249-
// { "dark", "light" }) into the two independent internal keys. A string sets both sides;
250-
// an object sets only the side(s) present (the missing side is left untouched so it
251-
// inherits / retains a prior layer's value). Matches main's LayerJson behavior.
245+
// { "dark", "light" }) into the two independent internal keys. A string sets both
246+
// sides; an object sets only the side(s) present, leaving the other to inherit.
252247
if (const auto& colorScheme{ json[JsonKey(ColorSchemeKey)] }; colorScheme.isString())
253248
{
254249
_json[JsonKey(DarkColorSchemeNameKey)] = colorScheme;

src/cascadia/TerminalSettingsModel/IInheritable.h

Lines changed: 180 additions & 460 deletions
Large diffs are not rendered by default.

src/cascadia/TerminalSettingsModel/JsonSyncCollections.h

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,15 @@ Module Name:
66
- JsonSyncCollections.h
77
88
Abstract:
9-
- IVector / IMap wrapper templates that mirror in-place mutations into a
10-
parent settings object's Json::Value _json. Required for "hybrid" settings
11-
whose JSON storage would otherwise miss .Append / .InsertAt / .Insert /
12-
.RemoveAt / etc. issued by the settings editor on the returned container.
13-
14-
Two backing flavors are supported, both via the same wrapper template:
15-
16-
1. Ephemeral shadow: caller passes a freshly-constructed IVector / IMap
17-
(seeded from the effective JSON value) as the backing. The wrapper is
18-
short-lived and discarded after use. _json is the only persistent
19-
source of truth. Used for DisabledProfileSources, FontAxes,
20-
FontFeatures, EnvironmentVariables.
21-
22-
2. Persistent backing: caller passes an existing long-lived IVector / IMap
23-
field on the parent (e.g. Profile::_BellSound) as the backing. The
24-
wrapper dual-writes to that field AND to _json. Used for BellSound,
25-
where IMediaResource resolution state must persist across getter calls.
26-
27-
The wrapper invokes an onChange callback after every mutation. The
28-
callback typically captures a strong ref to the parent and re-serializes
29-
the current backing into _json[key] via JsonUtils::SetValueForKey, then
30-
fires the WriteSettings signal (TODO GH#12424).
31-
32-
These templates intentionally implement IVector / IMap only — not
33-
IObservableVector / IObservableMap. No editor call site subscribes to
34-
VectorChanged / MapChanged on the returned collection; subscribers live
35-
on parallel VM-side observable collections.
9+
- IVector / IMap wrapper templates that mirror in-place mutations (.Append,
10+
.InsertAt, .RemoveAt, .Insert, …) into a parent settings object's _json via an
11+
onChange callback. Without this, editor mutations on a returned container would
12+
be lost. The backing may be an ephemeral shadow seeded from _json (the common
13+
case; _json is the source of truth) or a persistent field whose state must
14+
outlive the call (e.g. Profile::_BellSound, which holds IMediaResource
15+
resolution state).
16+
- IVector / IMap only — not the IObservable variants; nothing subscribes to
17+
VectorChanged / MapChanged here (VM-side collections handle that).
3618
3719
Author(s):
3820
- Carlos Zamora - June 2026

src/cascadia/TerminalSettingsModel/NewTabMenu.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
121121
{
122122
_entries = std::nullopt;
123123
_json = Json::Value{ Json::arrayValue };
124-
// TODO GH#12424: raise WriteSettings event
124+
// TODO GH#17000: raise WriteSettings event
125125
}
126126

127127
void NewTabMenu::ReplaceAll(const IVector<Model::NewTabMenuEntry>& entries)
@@ -377,7 +377,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
377377
}
378378
_json = std::move(arr);
379379
_emitChangeLog();
380-
// TODO GH#12424: raise WriteSettings event
380+
// TODO GH#17000: raise WriteSettings event
381381
}
382382

383383
void NewTabMenu::_emitChangeLog()

src/cascadia/TerminalSettingsModel/Profile.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
134134

135135
// Nullable/optional settings (JSON-backed)
136136
INHERITABLE_NULLABLE_SETTING(Model::Profile, Microsoft::Terminal::Core::Color, TabColor, "tabColor", nullptr)
137-
INHERITABLE_MUTABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr);
137+
138+
// UnfocusedAppearance holds a live AppearanceConfig sub-object whose
139+
// presence is the marker; it needs a backing field (not _json).
140+
_BASE_INHERITABLE_MUTABLE_SETTING(Model::Profile, std::optional<Model::IAppearanceConfig>, UnfocusedAppearance, nullptr)
141+
public:
142+
Model::IAppearanceConfig UnfocusedAppearance() const
143+
{
144+
const auto val{ _getUnfocusedAppearanceImpl() };
145+
return val ? *val : Model::IAppearanceConfig{ nullptr };
146+
}
147+
void UnfocusedAppearance(const Model::IAppearanceConfig& value)
148+
{
149+
_UnfocusedAppearance = value;
150+
}
151+
void ClearUnfocusedAppearance()
152+
{
153+
_UnfocusedAppearance = std::nullopt;
154+
}
138155

139156
// Settings that are JSON-backed but need custom handling in ToJson/LayerJson
140157
INHERITABLE_SETTING(Model::Profile, hstring, Name, "name", L"Default")
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#include "pch.h"
5+
6+
#include "../TerminalSettingsModel/JsonSyncCollections.h"
7+
8+
using namespace winrt::Windows::Foundation::Collections;
9+
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
10+
using namespace WEX::Logging;
11+
using namespace WEX::TestExecution;
12+
13+
namespace SettingsModelUnitTests
14+
{
15+
// Unit tests for the JsonSyncVector / JsonSyncMap wrappers in isolation
16+
// (with a stub onChange callback), independent of any settings object.
17+
class JsonSyncCollectionsTests
18+
{
19+
TEST_CLASS(JsonSyncCollectionsTests);
20+
21+
TEST_METHOD(VectorReadsDelegateToBacking);
22+
TEST_METHOD(VectorMutationsEachFireOnChangeOnce);
23+
TEST_METHOD(VectorOnChangeReceivesMutatedBacking);
24+
TEST_METHOD(VectorIterationWorks);
25+
TEST_METHOD(VectorNullOnChangeIsSafe);
26+
27+
TEST_METHOD(MapReadsDelegateToBacking);
28+
TEST_METHOD(MapMutationsEachFireOnChangeOnce);
29+
TEST_METHOD(MapOnChangeReceivesMutatedBacking);
30+
};
31+
32+
void JsonSyncCollectionsTests::VectorReadsDelegateToBacking()
33+
{
34+
const auto backing = winrt::single_threaded_vector<winrt::hstring>({ L"a", L"b", L"c" });
35+
auto fired = 0;
36+
const IVector<winrt::hstring> wrapper = winrt::make<JsonSyncVector<winrt::hstring>>(
37+
backing, [&](IVector<winrt::hstring> const&) { ++fired; });
38+
39+
VERIFY_ARE_EQUAL(3u, wrapper.Size());
40+
VERIFY_ARE_EQUAL(L"b", wrapper.GetAt(1));
41+
42+
uint32_t index{};
43+
VERIFY_IS_TRUE(wrapper.IndexOf(L"c", index));
44+
VERIFY_ARE_EQUAL(2u, index);
45+
46+
VERIFY_ARE_EQUAL(3u, wrapper.GetView().Size());
47+
48+
// Reads must not trigger the sync callback.
49+
VERIFY_ARE_EQUAL(0, fired);
50+
}
51+
52+
void JsonSyncCollectionsTests::VectorMutationsEachFireOnChangeOnce()
53+
{
54+
const auto backing = winrt::single_threaded_vector<winrt::hstring>({ L"a" });
55+
auto fired = 0;
56+
const IVector<winrt::hstring> wrapper = winrt::make<JsonSyncVector<winrt::hstring>>(
57+
backing, [&](IVector<winrt::hstring> const&) { ++fired; });
58+
59+
wrapper.Append(L"b");
60+
VERIFY_ARE_EQUAL(1, fired);
61+
62+
wrapper.InsertAt(0, L"z");
63+
VERIFY_ARE_EQUAL(2, fired);
64+
65+
wrapper.SetAt(0, L"y");
66+
VERIFY_ARE_EQUAL(3, fired);
67+
68+
wrapper.RemoveAt(0);
69+
VERIFY_ARE_EQUAL(4, fired);
70+
71+
wrapper.RemoveAtEnd();
72+
VERIFY_ARE_EQUAL(5, fired);
73+
74+
const std::array<winrt::hstring, 2> replacement{ L"p", L"q" };
75+
wrapper.ReplaceAll(replacement);
76+
VERIFY_ARE_EQUAL(6, fired);
77+
VERIFY_ARE_EQUAL(2u, wrapper.Size());
78+
79+
wrapper.Clear();
80+
VERIFY_ARE_EQUAL(7, fired);
81+
VERIFY_ARE_EQUAL(0u, wrapper.Size());
82+
}
83+
84+
void JsonSyncCollectionsTests::VectorOnChangeReceivesMutatedBacking()
85+
{
86+
const auto backing = winrt::single_threaded_vector<winrt::hstring>({ L"a" });
87+
uint32_t lastSize{};
88+
const IVector<winrt::hstring> wrapper = winrt::make<JsonSyncVector<winrt::hstring>>(
89+
backing, [&](IVector<winrt::hstring> const& current) { lastSize = current.Size(); });
90+
91+
wrapper.Append(L"b");
92+
// The callback sees the already-mutated backing, and the mutation lands
93+
// on the shared backing the caller passed in.
94+
VERIFY_ARE_EQUAL(2u, lastSize);
95+
VERIFY_ARE_EQUAL(2u, backing.Size());
96+
VERIFY_ARE_EQUAL(L"b", backing.GetAt(1));
97+
}
98+
99+
void JsonSyncCollectionsTests::VectorIterationWorks()
100+
{
101+
const auto backing = winrt::single_threaded_vector<winrt::hstring>({ L"x", L"y" });
102+
const IVector<winrt::hstring> wrapper = winrt::make<JsonSyncVector<winrt::hstring>>(
103+
backing, nullptr);
104+
105+
std::vector<winrt::hstring> seen;
106+
for (const auto& item : wrapper)
107+
{
108+
seen.push_back(item);
109+
}
110+
VERIFY_ARE_EQUAL(2u, static_cast<uint32_t>(seen.size()));
111+
VERIFY_ARE_EQUAL(L"x", seen[0]);
112+
VERIFY_ARE_EQUAL(L"y", seen[1]);
113+
}
114+
115+
void JsonSyncCollectionsTests::VectorNullOnChangeIsSafe()
116+
{
117+
const auto backing = winrt::single_threaded_vector<winrt::hstring>();
118+
const IVector<winrt::hstring> wrapper = winrt::make<JsonSyncVector<winrt::hstring>>(
119+
backing, nullptr);
120+
121+
// A null onChange must be tolerated (the wrapper guards the callback).
122+
wrapper.Append(L"a");
123+
VERIFY_ARE_EQUAL(1u, wrapper.Size());
124+
}
125+
126+
void JsonSyncCollectionsTests::MapReadsDelegateToBacking()
127+
{
128+
const auto backing = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
129+
backing.Insert(L"k1", L"v1");
130+
131+
auto fired = 0;
132+
const IMap<winrt::hstring, winrt::hstring> wrapper = winrt::make<JsonSyncMap<winrt::hstring, winrt::hstring>>(
133+
backing, [&](IMap<winrt::hstring, winrt::hstring> const&) { ++fired; });
134+
135+
VERIFY_ARE_EQUAL(1u, wrapper.Size());
136+
VERIFY_IS_TRUE(wrapper.HasKey(L"k1"));
137+
VERIFY_IS_FALSE(wrapper.HasKey(L"nope"));
138+
VERIFY_ARE_EQUAL(L"v1", wrapper.Lookup(L"k1"));
139+
VERIFY_ARE_EQUAL(1u, wrapper.GetView().Size());
140+
141+
VERIFY_ARE_EQUAL(0, fired);
142+
}
143+
144+
void JsonSyncCollectionsTests::MapMutationsEachFireOnChangeOnce()
145+
{
146+
const auto backing = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
147+
auto fired = 0;
148+
const IMap<winrt::hstring, winrt::hstring> wrapper = winrt::make<JsonSyncMap<winrt::hstring, winrt::hstring>>(
149+
backing, [&](IMap<winrt::hstring, winrt::hstring> const&) { ++fired; });
150+
151+
VERIFY_IS_FALSE(wrapper.Insert(L"k1", L"v1")); // new key → not a replacement
152+
VERIFY_ARE_EQUAL(1, fired);
153+
154+
VERIFY_IS_TRUE(wrapper.Insert(L"k1", L"v2")); // existing key → replacement
155+
VERIFY_ARE_EQUAL(2, fired);
156+
157+
wrapper.Remove(L"k1");
158+
VERIFY_ARE_EQUAL(3, fired);
159+
160+
wrapper.Clear();
161+
VERIFY_ARE_EQUAL(4, fired);
162+
}
163+
164+
void JsonSyncCollectionsTests::MapOnChangeReceivesMutatedBacking()
165+
{
166+
const auto backing = winrt::single_threaded_map<winrt::hstring, winrt::hstring>();
167+
uint32_t lastSize{};
168+
const IMap<winrt::hstring, winrt::hstring> wrapper = winrt::make<JsonSyncMap<winrt::hstring, winrt::hstring>>(
169+
backing, [&](IMap<winrt::hstring, winrt::hstring> const& current) { lastSize = current.Size(); });
170+
171+
wrapper.Insert(L"k1", L"v1");
172+
VERIFY_ARE_EQUAL(1u, lastSize);
173+
VERIFY_ARE_EQUAL(1u, backing.Size());
174+
VERIFY_ARE_EQUAL(L"v1", backing.Lookup(L"k1"));
175+
}
176+
}

src/cascadia/UnitTests_SettingsModel/ProfileTests.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ namespace SettingsModelUnitTests
5151

5252
TEST_METHOD(ClearIconAlsoClearsJson);
5353
TEST_METHOD(ClearIconAndBellSoundAreLogged);
54-
54+
5555
TEST_METHOD(SettingInheritanceFallback);
5656
TEST_METHOD(ClearSettingRestoresInheritance);
5757
TEST_METHOD(HasSettingAtSpecificLayer);
@@ -844,7 +844,7 @@ namespace SettingsModelUnitTests
844844
// NOTE: CascadiaSettings::_validateAllSchemesExist() runs during construction and
845845
// clears any DarkColorSchemeName/LightColorSchemeName that doesn't name a registered
846846
// scheme (falling back to the default). So every scheme the cases below reference must
847-
// be registered via this inbox JSON, otherwise the loaded values get wiped on load.
847+
// be registered via this inbox JSON; otherwise the loaded values get wiped on load.
848848
static constexpr std::string_view inboxSchemes{ R"({
849849
"schemes": [
850850
{ "name": "Campbell", "black": "#0C0C0C", "red": "#C50F1F", "green": "#13A10E", "yellow": "#C19C00", "blue": "#0037DA", "purple": "#881798", "cyan": "#3A96DD", "white": "#CCCCCC", "brightBlack": "#767676", "brightRed": "#E74856", "brightGreen": "#16C60C", "brightYellow": "#F9F1A5", "brightBlue": "#3B78FF", "brightPurple": "#B4009E", "brightCyan": "#61D6D6", "brightWhite": "#F2F2F2" },
@@ -1390,7 +1390,7 @@ namespace SettingsModelUnitTests
13901390
VERIFY_IS_TRUE(changes.contains("profile.bellSound"));
13911391
}
13921392

1393-
void ProfileTests::SettingInheritanceFallback()
1393+
void ProfileTests::SettingInheritanceFallback()
13941394
{
13951395
// Verify that when no layer defines a setting, the default value is used.
13961396
// Also verify that when only user defaults defines it, profiles inherit from there.

src/cascadia/UnitTests_SettingsModel/SettingsModel.UnitTests.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<ClCompile Include="KeyBindingsTests.cpp" />
4141
<ClCompile Include="CommandTests.cpp" />
4242
<ClCompile Include="DeserializationTests.cpp" />
43+
<ClCompile Include="JsonSyncCollectionsTests.cpp" />
4344
<ClCompile Include="NewTabMenuTests.cpp" />
4445
<ClCompile Include="SerializationTests.cpp" />
4546
<ClCompile Include="TerminalSettingsTests.cpp" />

0 commit comments

Comments
 (0)