Skip to content

Commit 758fdf2

Browse files
committed
fix(settings): keep lane override badge and reset across capsule groups
1 parent a239b48 commit 758fdf2

13 files changed

Lines changed: 455 additions & 19 deletions

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,7 @@ if build_tests
10941094
'caldav_client',
10951095
'calendar_credential_store',
10961096
'calendar_discovery_state',
1097+
'capsule_group_lane_override',
10971098
'capsule_group_reconcile',
10981099
'clipboard_service',
10991100
'clipboard_storage_permissions',

src/config/config_overrides.cpp

Lines changed: 201 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,106 @@ namespace {
734734
return it != bar.monitorOverrides.end() ? &*it : nullptr;
735735
}
736736

737+
// {"bar", name, lane} or {"bar", name, "monitor", match, lane}, lane being a widget list.
738+
bool isBarLanePath(const std::vector<std::string>& path) {
739+
const bool barScope = path.size() == 3 && path[0] == "bar";
740+
const bool monitorScope = path.size() == 5 && path[0] == "bar" && path[2] == "monitor";
741+
if (!barScope && !monitorScope) {
742+
return false;
743+
}
744+
const std::string& lane = path.back();
745+
return lane == "start" || lane == "center" || lane == "end";
746+
}
747+
748+
std::vector<std::string> capsuleGroupPathForBarLanePath(const std::vector<std::string>& lanePath) {
749+
std::vector<std::string> path(lanePath.begin(), lanePath.end() - 1);
750+
path.emplace_back("capsule_group");
751+
return path;
752+
}
753+
754+
const std::vector<std::string>* barLaneWidgets(const Config& cfg, const std::vector<std::string>& lanePath) {
755+
const BarConfig* bar = findBarConfig(cfg, lanePath[1]);
756+
if (bar == nullptr) {
757+
return nullptr;
758+
}
759+
const std::string& lane = lanePath.back();
760+
if (lanePath.size() == 5) {
761+
const BarMonitorOverride* ovr = findBarMonitorOverride(*bar, lanePath[3]);
762+
if (ovr == nullptr) {
763+
return nullptr;
764+
}
765+
const std::optional<std::vector<std::string>>& laneOverride =
766+
lane == "start" ? ovr->startWidgets : (lane == "center" ? ovr->centerWidgets : ovr->endWidgets);
767+
if (laneOverride.has_value()) {
768+
return &*laneOverride;
769+
}
770+
}
771+
return lane == "start" ? &bar->startWidgets : (lane == "center" ? &bar->centerWidgets : &bar->endWidgets);
772+
}
773+
774+
const std::vector<BarCapsuleGroupStyle>*
775+
barLaneCapsuleGroups(const Config& cfg, const std::vector<std::string>& lanePath) {
776+
const BarConfig* bar = findBarConfig(cfg, lanePath[1]);
777+
if (bar == nullptr) {
778+
return nullptr;
779+
}
780+
if (lanePath.size() == 5) {
781+
const BarMonitorOverride* ovr = findBarMonitorOverride(*bar, lanePath[3]);
782+
if (ovr != nullptr && ovr->widgetCapsuleGroups.has_value()) {
783+
return &*ovr->widgetCapsuleGroups;
784+
}
785+
}
786+
return &bar->widgetCapsuleGroups;
787+
}
788+
789+
void collectLaneGroupIds(const std::vector<std::string>& lane, std::set<std::string>& out) {
790+
for (const std::string& entry : lane) {
791+
if (isCapsuleGroupToken(entry)) {
792+
out.insert(capsuleGroupTokenId(entry));
793+
}
794+
}
795+
}
796+
797+
// Lane equality as the settings GUI presents it: the widget list plus the styles of the capsule
798+
// groups that list references. Groups the lane does not reference belong to another lane.
799+
bool barLaneContentEqual(const Config& a, const Config& b, const std::vector<std::string>& lanePath) {
800+
const std::vector<std::string>* laneA = barLaneWidgets(a, lanePath);
801+
const std::vector<std::string>* laneB = barLaneWidgets(b, lanePath);
802+
if (laneA == nullptr || laneB == nullptr) {
803+
return laneA == laneB;
804+
}
805+
if (*laneA != *laneB) {
806+
return false;
807+
}
808+
const std::vector<BarCapsuleGroupStyle>* groupsA = barLaneCapsuleGroups(a, lanePath);
809+
const std::vector<BarCapsuleGroupStyle>* groupsB = barLaneCapsuleGroups(b, lanePath);
810+
for (const std::string& entry : *laneA) {
811+
if (!isCapsuleGroupToken(entry)) {
812+
continue;
813+
}
814+
const std::string id = capsuleGroupTokenId(entry);
815+
const auto findGroup = [&id](const std::vector<BarCapsuleGroupStyle>* groups) -> const BarCapsuleGroupStyle* {
816+
if (groups == nullptr) {
817+
return nullptr;
818+
}
819+
const auto it = std::ranges::find(*groups, id, &BarCapsuleGroupStyle::id);
820+
return it != groups->end() ? &*it : nullptr;
821+
};
822+
const BarCapsuleGroupStyle* groupA = findGroup(groupsA);
823+
const BarCapsuleGroupStyle* groupB = findGroup(groupsB);
824+
if (groupA == nullptr || groupB == nullptr) {
825+
if (groupA != groupB) {
826+
return false;
827+
}
828+
continue;
829+
}
830+
if (*groupA != *groupB) {
831+
return false;
832+
}
833+
}
834+
return true;
835+
}
836+
737837
bool overridePresenceIsSemantic(const std::vector<std::string>& path) {
738838
if (path.size() == 3 && path[0] == "widget" && path[2] == "type") {
739839
return true;
@@ -1184,6 +1284,35 @@ bool ConfigService::hasEffectiveOverride(const std::vector<std::string>& path) c
11841284
return effective;
11851285
}
11861286

1287+
bool ConfigService::hasEffectiveBarLaneOverride(const std::vector<std::string>& lanePath) const {
1288+
if (!isBarLanePath(lanePath)) {
1289+
return false;
1290+
}
1291+
if (hasEffectiveOverride(lanePath)) {
1292+
return true;
1293+
}
1294+
1295+
// The lane list itself matches the config file, but a group token in it can still carry the
1296+
// change: moving a widget into a lane's capsule group only edits the scope's capsule_group array.
1297+
const std::vector<std::string> groupPath = capsuleGroupPathForBarLanePath(lanePath);
1298+
if (findOverrideNode(m_overridesTable, groupPath) == nullptr) {
1299+
return false;
1300+
}
1301+
1302+
const std::string key = "lane:" + overrideCacheKey(lanePath);
1303+
if (const auto it = m_effectiveOverrideCache.find(key); it != m_effectiveOverrideCache.end()) {
1304+
return it->second;
1305+
}
1306+
1307+
toml::table without = m_overridesTable;
1308+
eraseOverridePath(without, lanePath, overridePreserveDepthForPath(lanePath));
1309+
eraseOverridePath(without, groupPath, overridePreserveDepthForPath(groupPath));
1310+
const auto baseline = configForOverrides(without);
1311+
const bool effective = !baseline.has_value() || !barLaneContentEqual(m_config, *baseline, lanePath);
1312+
m_effectiveOverrideCache[key] = effective;
1313+
return effective;
1314+
}
1315+
11871316
std::size_t ConfigService::overridePreserveDepthForPath(const std::vector<std::string>& path) const {
11881317
if (path.size() > 4 && path[0] == "bar" && path[2] == "monitor" && isOverrideOnlyMonitorOverride(path[1], path[3])) {
11891318
return 4;
@@ -1768,6 +1897,10 @@ bool ConfigService::setOverrides(
17681897
}
17691898
}
17701899

1900+
return commitOverrideTable(std::move(next), changed);
1901+
}
1902+
1903+
bool ConfigService::commitOverrideTable(toml::table next, bool* changed) {
17711904
if (next == m_overridesTable) {
17721905
m_lastMutationError.clear();
17731906
return true;
@@ -1832,26 +1965,80 @@ bool ConfigService::clearOverrides(const std::vector<std::vector<std::string>>&
18321965

18331966
reconcileCapsuleGroupOverrides(next);
18341967

1835-
if (!validateOverrideMutation(next)) {
1968+
return commitOverrideTable(std::move(next), changed);
1969+
}
1970+
1971+
bool ConfigService::resetBarLaneOverride(const std::vector<std::string>& lanePath, bool* changed) {
1972+
if (changed != nullptr) {
1973+
*changed = false;
1974+
}
1975+
if (m_overridesPath.empty() || !isBarLanePath(lanePath)) {
18361976
return false;
18371977
}
18381978

1839-
toml::table previous = std::move(m_overridesTable);
1840-
m_overridesTable = std::move(next);
1841-
if (!writeOverridesToFile()) {
1842-
m_overridesTable = std::move(previous);
1843-
kLog.warn("failed to write {}", m_overridesPath);
1844-
return false;
1979+
toml::table next = m_overridesTable;
1980+
bool anyChanged = eraseOverridePath(next, lanePath, overridePreserveDepthForPath(lanePath));
1981+
1982+
// Restore the groups this lane holds. The scope's capsule_group array is shared with the other
1983+
// lanes, so it is rewritten rather than cleared: only ids this lane references go back to their
1984+
// config-file style, and GUI-created ones disappear with it.
1985+
const std::vector<std::string> groupPath = capsuleGroupPathForBarLanePath(lanePath);
1986+
if (findOverrideNode(next, groupPath) != nullptr) {
1987+
toml::table baselineTable = next;
1988+
eraseOverridePath(baselineTable, groupPath, overridePreserveDepthForPath(groupPath));
1989+
const auto baseline = configForOverrides(baselineTable);
1990+
if (!baseline.has_value()) {
1991+
return false;
1992+
}
1993+
const std::vector<BarCapsuleGroupStyle>* baseGroups = barLaneCapsuleGroups(*baseline, lanePath);
1994+
// Current state comes from the live config: `next` already dropped the lane override, so its
1995+
// lane no longer names the groups that exist only because of it.
1996+
const std::vector<BarCapsuleGroupStyle>* currentGroups = barLaneCapsuleGroups(m_config, lanePath);
1997+
const std::vector<std::string>* baseLane = barLaneWidgets(*baseline, lanePath);
1998+
const std::vector<std::string>* currentLane = barLaneWidgets(m_config, lanePath);
1999+
if (baseGroups != nullptr && currentGroups != nullptr && baseLane != nullptr && currentLane != nullptr) {
2000+
std::set<std::string> owned;
2001+
collectLaneGroupIds(*baseLane, owned);
2002+
collectLaneGroupIds(*currentLane, owned);
2003+
2004+
std::vector<BarCapsuleGroupStyle> restored;
2005+
restored.reserve(currentGroups->size());
2006+
for (const auto& group : *currentGroups) {
2007+
if (!owned.contains(group.id)) {
2008+
restored.push_back(group);
2009+
continue;
2010+
}
2011+
const auto it = std::ranges::find(*baseGroups, group.id, &BarCapsuleGroupStyle::id);
2012+
if (it != baseGroups->end()) {
2013+
restored.push_back(*it);
2014+
}
2015+
}
2016+
for (const auto& group : *baseGroups) {
2017+
if (owned.contains(group.id) && !std::ranges::contains(restored, group.id, &BarCapsuleGroupStyle::id)) {
2018+
restored.push_back(group);
2019+
}
2020+
}
2021+
2022+
if (restored != *currentGroups) {
2023+
toml::table* scope = &next;
2024+
for (std::size_t i = 0; i + 1 < groupPath.size() && scope != nullptr; ++i) {
2025+
scope = scope->get_as<toml::table>(groupPath[i]);
2026+
}
2027+
if (scope != nullptr) {
2028+
insertOverrideValue(*scope, groupPath.back(), restored);
2029+
anyChanged = true;
2030+
}
2031+
}
2032+
}
18452033
}
18462034

1847-
m_ownOverridesWritePending = true;
1848-
if (changed != nullptr) {
1849-
*changed = true;
2035+
if (!anyChanged) {
2036+
m_lastMutationError.clear();
2037+
return true;
18502038
}
1851-
extractWallpaperFromOverrides();
1852-
loadAll();
1853-
fireReloadCallbacks();
1854-
return true;
2039+
2040+
reconcileCapsuleGroupOverrides(next);
2041+
return commitOverrideTable(std::move(next), changed);
18552042
}
18562043

18572044
bool ConfigService::renameOverrideTable(

src/config/config_service.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ class ConfigService {
134134
bool markSetupWizardCompleted();
135135
[[nodiscard]] bool hasOverride(const std::vector<std::string>& path) const;
136136
[[nodiscard]] bool hasEffectiveOverride(const std::vector<std::string>& path) const;
137+
// Lane content as the settings GUI presents it: the widget list plus the capsule groups it
138+
// references, so moving a widget into a lane's group still reports the lane as overridden.
139+
[[nodiscard]] bool hasEffectiveBarLaneOverride(const std::vector<std::string>& lanePath) const;
140+
// Reverts a lane to the config file: drops the lane-list override and restores the capsule groups
141+
// that lane references, leaving groups owned by other lanes alone.
142+
bool resetBarLaneOverride(const std::vector<std::string>& lanePath, bool* changed = nullptr);
137143
[[nodiscard]] bool isOverrideOnlyBar(std::string_view name) const;
138144
[[nodiscard]] bool isOverrideOnlyCalendarAccount(std::string_view id) const;
139145
[[nodiscard]] bool canMoveBarOverride(std::string_view name, int direction) const;
@@ -183,6 +189,8 @@ class ConfigService {
183189
// config-file lane whose group token the override no longer defines. Rewrites the affected
184190
// arrays in `candidate` so every referenced group resolves again.
185191
void reconcileCapsuleGroupOverrides(toml::table& candidate) const;
192+
// Validates `next`, persists it, and reloads. `changed` reports whether anything moved.
193+
bool commitOverrideTable(toml::table next, bool* changed);
186194
void setupWatch();
187195
// Reconciles inotify watches for [include]d files: watches the parent dir of
188196
// every loaded file plus every directory named in an [include].files list, and

src/shell/settings/bar_widget_editor.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3003,7 +3003,9 @@ namespace settings {
30033003
for (const auto laneKey : kLaneKeys) {
30043004
auto lanePath = pathWithLastSegment(entry.path, std::string(laneKey));
30053005
const auto laneItems = barWidgetItemsForPath(ctx.config, lanePath);
3006-
const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(lanePath);
3006+
// Lane content includes the styles of the capsule groups it holds, so an edit that only lands
3007+
// in the scope's capsule_group array still marks its lane as overridden.
3008+
const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveBarLaneOverride(lanePath);
30073009
const bool hasGuiOverride = ctx.configService != nullptr && ctx.configService->hasOverride(lanePath);
30083010
const bool monitorLaneExplicit = monitorWidgetListHasExplicitValue(ctx.config, lanePath);
30093011
const bool inherited = isMonitorWidgetListPath(lanePath) && !monitorLaneExplicit;
@@ -3108,8 +3110,11 @@ namespace settings {
31083110
})
31093111
);
31103112
}
3113+
// Reset reverts the whole lane: its widget list and the capsule groups it holds.
31113114
if (overridden || (monitorLaneExplicit && hasGuiOverride)) {
3112-
laneHeader->addChild(ctx.makeResetButton(lanePath));
3115+
laneHeader->addChild(ctx.makeResetActionButton(lanePath, [resetBarLane = ctx.resetBarLane, lanePath]() {
3116+
resetBarLane(lanePath);
3117+
}));
31133118
}
31143119
lane->addChild(std::move(laneHeader));
31153120

src/shell/settings/bar_widget_editor.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,17 @@ namespace settings {
4747
std::function<void(std::vector<std::string>, ConfigOverrideValue)> setOverride;
4848
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
4949
std::function<void(std::vector<std::string>)> clearOverride;
50+
// Reverts a lane and the capsule groups it holds to the config file.
51+
std::function<void(std::vector<std::string>)> resetBarLane;
5052
std::function<void(std::string, std::string, std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)>
5153
renameWidgetInstance;
5254
std::function<void()> closeHostedEditor;
5355
std::function<void(std::vector<std::string> laneListPath, std::string widgetName)> openWidgetInspector;
5456
std::function<void(std::vector<std::string> laneListPath, std::string groupId)> openCapsuleGroupInspector;
5557
std::function<std::unique_ptr<Button>(const std::vector<std::string>&)> makeResetButton;
58+
// Reset-styled button (with the usual confirm step) that runs `action` instead of a plain clear.
59+
std::function<std::unique_ptr<Button>(const std::vector<std::string>&, std::function<void()>)>
60+
makeResetActionButton;
5661
std::function<void(Flex&, const SettingEntry&, std::unique_ptr<Node>)> makeRow;
5762
std::function<std::unique_ptr<Node>(bool, std::vector<std::string>, std::optional<bool> clearWhenValue)> makeToggle;
5863
std::function<std::unique_ptr<Node>(const SelectSetting&, std::vector<std::string>)> makeSelect;

src/shell/settings/settings_content.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,15 @@ namespace settings {
8989
.setOverride = ctx.setOverride,
9090
.setOverrides = ctx.setOverrides,
9191
.clearOverride = ctx.clearOverride,
92+
.resetBarLane = ctx.resetBarLane,
9293
.renameWidgetInstance = ctx.renameWidgetInstance,
9394
.closeHostedEditor = ctx.closeHostedEditor,
9495
.openWidgetInspector = ctx.openWidgetInspectorEditor,
9596
.openCapsuleGroupInspector = ctx.openCapsuleGroupEditor,
9697
.makeResetButton = [&factory](const std::vector<std::string>& path) { return factory.makeResetButton(path); },
98+
.makeResetActionButton = [&factory](
99+
const std::vector<std::string>& path, std::function<void()> action
100+
) { return factory.makeResetButton(path, std::move(action)); },
97101
.makeRow = [&factory](
98102
Flex& section, const SettingEntry& entry, std::unique_ptr<Node> control
99103
) { factory.makeRow(section, entry, std::move(control)); },

src/shell/settings/settings_content.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ namespace settings {
7272
std::function<void(std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)> setOverrides;
7373
std::function<void(std::vector<std::string>)> clearOverride;
7474
std::function<void(std::vector<std::vector<std::string>>)> clearOverrides;
75+
std::function<void(std::vector<std::string>)> resetBarLane;
7576
std::function<bool(const std::vector<std::vector<std::string>>&)> isResetConfirmationPending;
7677
std::function<void(std::vector<std::vector<std::string>>)> requestResetConfirmation;
7778
std::function<void(std::string, std::string, std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)>

src/shell/settings/settings_control_factory.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,14 @@ namespace settings {
173173
return makeGroupedResetButton(std::vector<std::vector<std::string>>{path});
174174
}
175175

176-
std::unique_ptr<Button> SettingsControlFactory::makeGroupedResetButton(std::vector<std::vector<std::string>> paths) {
176+
std::unique_ptr<Button>
177+
SettingsControlFactory::makeResetButton(const std::vector<std::string>& path, std::function<void()> onConfirmed) {
178+
return makeGroupedResetButton(std::vector<std::vector<std::string>>{path}, std::move(onConfirmed));
179+
}
180+
181+
std::unique_ptr<Button> SettingsControlFactory::makeGroupedResetButton(
182+
std::vector<std::vector<std::string>> paths, std::function<void()> onConfirmed
183+
) {
177184
auto& ctx = m_ctx;
178185
const float scale = m_scale;
179186
const bool pendingConfirmation = ctx.isResetConfirmationPending && ctx.isResetConfirmationPending(paths);
@@ -186,12 +193,17 @@ namespace settings {
186193
.paddingH = Style::spaceSm * scale,
187194
.radius = Style::scaledRadiusMd(scale),
188195
.onClick = [clearOverrides = ctx.clearOverrides, requestConfirmation = ctx.requestResetConfirmation,
189-
requestRebuild = ctx.requestRebuild, paths = std::move(paths), pendingConfirmation]() mutable {
196+
requestRebuild = ctx.requestRebuild, paths = std::move(paths), onConfirmed = std::move(onConfirmed),
197+
pendingConfirmation]() mutable {
190198
if (!pendingConfirmation) {
191199
requestConfirmation(paths);
192200
requestRebuild();
193201
return;
194202
}
203+
if (onConfirmed) {
204+
onConfirmed();
205+
return;
206+
}
195207
clearOverrides(std::move(paths));
196208
},
197209
});

src/shell/settings/settings_control_factory.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ namespace settings {
3030
[[nodiscard]] float scale() const noexcept { return m_scale; }
3131

3232
[[nodiscard]] std::unique_ptr<Button> makeResetButton(const std::vector<std::string>& path);
33+
// Same button, but the confirmed click runs `onConfirmed` instead of clearing `path`.
34+
[[nodiscard]] std::unique_ptr<Button>
35+
makeResetButton(const std::vector<std::string>& path, std::function<void()> onConfirmed);
3336

3437
void makeRow(Flex& section, const SettingEntry& entry, std::unique_ptr<Node> control);
3538

@@ -93,7 +96,8 @@ namespace settings {
9396
[[nodiscard]] std::unique_ptr<Flex> makeOverrideBadge();
9497
[[nodiscard]] std::unique_ptr<Flex> makeAdvancedBadge();
9598
// Resets several config paths as one setting (e.g. a range slider's low + high paths).
96-
[[nodiscard]] std::unique_ptr<Button> makeGroupedResetButton(std::vector<std::vector<std::string>> paths);
99+
[[nodiscard]] std::unique_ptr<Button>
100+
makeGroupedResetButton(std::vector<std::vector<std::string>> paths, std::function<void()> onConfirmed = nullptr);
97101
[[nodiscard]] static bool isTemplateEnableTogglePath(const std::vector<std::string>& path);
98102

99103
SettingsContentContext m_ctx;

0 commit comments

Comments
 (0)