Patch settings-only bar config changes in place instead of rebuilding every widget

A shell.json write used to reassign the whole layout, and the module
Repeaters recreate every delegate when their array model changes — so
toggling an inline widget setting (battery percentage, clock format,
tray pinning) tore down and rebuilt every widget on every monitor,
closing any open panel along the way. When the layout structure is
unchanged, hand the new settings to the running widgets instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-01 15:36:34 -05:00
co-authored by Claude Fable 5
parent c0d4037237
commit 1f10c78c6a
3 changed files with 107 additions and 1 deletions
+26 -1
View File
@@ -350,10 +350,35 @@ Item {
position = normalizePosition(config.position)
setRequestedTransparency(config.transparent === true)
centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "")
layoutConfig = normalizeLayout(config.layout)
// layoutEntries feeds plain JS arrays to the module Repeaters, and QML
// cannot diff those: reassigning layoutConfig rebuilds every widget on
// every monitor. When a shell.json write only changed inline widget
// settings, patch the live layout and running widgets in place instead.
var next = normalizeLayout(config.layout)
var delta = BarModel.inlineSettingsDelta(layoutConfig, next)
if (delta) {
applySettingsDelta(delta)
return
}
layoutConfig = next
barConfigSerial++
}
function applySettingsDelta(delta) {
for (var i = 0; i < delta.length; i++) {
var change = delta[i]
layoutConfig[change.region][change.index] = change.entry
var settings = entrySettings(change.entry)
for (var s = 0; s < moduleSlots.length; s++) {
var slot = moduleSlots[s]
if (!slot || slot.region !== change.region || slot.moduleName !== entryId(change.entry)) continue
var item = slot.activeItem
if (item && "settings" in item) item.settings = settings
}
}
}
onBarConfigChanged: applyBarConfig()
function layoutEntries(region) {
+37
View File
@@ -65,6 +65,42 @@ function entriesAfter(entries, name) {
return index === -1 ? [] : entries.slice(index + 1)
}
// A shell.json write that only changes inline widget settings (the battery
// percentage toggle, a clock format change) must not rebuild the bar.
// Compare two normalized layouts: when the structure is unchanged — same
// entry ids in the same order per region — return the settings-only changes
// as {region, index, entry}. Return null when the change is structural, or
// touches an entry a live settings push cannot safely reach: custom modules
// read their entry directly rather than an injected settings property, and
// a duplicated id makes the push ambiguous.
function inlineSettingsDelta(current, next) {
if (!isPlainObject(current) || !isPlainObject(next)) return null
var regions = ["left", "center", "right"]
var counts = {}
for (var r = 0; r < regions.length; r++) {
var entries = Array.isArray(next[regions[r]]) ? next[regions[r]] : []
for (var i = 0; i < entries.length; i++) {
var id = entryId(entries[i])
counts[id] = (counts[id] || 0) + 1
}
}
var changes = []
for (var s = 0; s < regions.length; s++) {
var region = regions[s]
var a = Array.isArray(current[region]) ? current[region] : []
var b = Array.isArray(next[region]) ? next[region] : []
if (a.length !== b.length) return null
for (var j = 0; j < a.length; j++) {
if (entryId(a[j]) !== entryId(b[j])) return null
if (JSON.stringify(a[j]) === JSON.stringify(b[j])) continue
if (customModuleType(a[j]) || customModuleType(b[j])) return null
if (counts[entryId(b[j])] > 1) return null
changes.push({ region: region, index: j, entry: b[j] })
}
}
return changes
}
function expandPath(value, home) {
var path = String(value || "")
if (path === "") return ""
@@ -162,6 +198,7 @@ if (typeof module !== "undefined") {
entryIndex: entryIndex,
entriesBefore: entriesBefore,
entriesAfter: entriesAfter,
inlineSettingsDelta: inlineSettingsDelta,
expandPath: expandPath,
customModuleSafeName: customModuleSafeName,
customModuleType: customModuleType,