From c2610c3788a928cb39aaff1ffc54ca1e8c573b37 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 26 Jul 2026 19:10:06 -0700 Subject: [PATCH] Give the weather panel its host widget's identity in the bar The bar identifies a panel by the widget mounted in its slot, but the nested panel handed the popout coordinator itself. The open-panel mark never lit under the weather pill, and Tab could not leave the panel. Closing for a popout switch also cleared the shared hover-reveal flag the incoming panel had just set. Co-Authored-By: Claude Opus 5 (1M context) --- shell/plugins/panels/weather/BarWidget.qml | 10 +++++++++ shell/plugins/panels/weather/Panel.qml | 21 ++++++++++++++++-- test/shell.d/weather-test.sh | 25 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/shell/plugins/panels/weather/BarWidget.qml b/shell/plugins/panels/weather/BarWidget.qml index 667e9905..845c23d3 100644 --- a/shell/plugins/panels/weather/BarWidget.qml +++ b/shell/plugins/panels/weather/BarWidget.qml @@ -12,6 +12,7 @@ BarWidget { if ("bar" in target) target.bar = root.bar if ("settings" in target) target.settings = root.settings if ("anchorItem" in target) target.anchorItem = button + if ("hostWidget" in target) target.hostWidget = root } function refresh() { @@ -36,6 +37,15 @@ BarWidget { if (panelLoader.item && panelLoader.item.close) panelLoader.item.close() } + // Forwarded so this widget can stand in for the panel as the bar's popout + // identity: Bar.requestPopout prefers closeForPopoutSwitch over close, and + // KeyboardPanel reads popoutSwitchClosing back off its owner. + readonly property bool popoutSwitchClosing: panelLoader.item ? panelLoader.item.popoutSwitchClosing === true : false + + function closeForPopoutSwitch() { + if (panelLoader.item) panelLoader.item.closeForPopoutSwitch() + } + visible: panelLoader.item && panelLoader.item.label !== "" implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight diff --git a/shell/plugins/panels/weather/Panel.qml b/shell/plugins/panels/weather/Panel.qml index a4f86c82..35df14f9 100644 --- a/shell/plugins/panels/weather/Panel.qml +++ b/shell/plugins/panels/weather/Panel.qml @@ -15,6 +15,14 @@ Panel { property var anchorItem: null property bool openedFromHotkey: false + // The bar tracks the widget mounted in its slot — BarWidget.qml — not this + // nested panel. Everything the bar identifies a panel by has to be that + // widget: the popout coordinator (and with it the open-panel dot under the + // pill) compares against `slot.activeItem`, and switchPanelFrom looks the + // slot up the same way. + property var hostWidget: null + readonly property var barIdentity: hostWidget || root + function open() { openedFromHotkey = false setCenterHoverRevealSuppressed(false) @@ -32,7 +40,10 @@ Panel { } function close() { - setCenterHoverRevealSuppressed(false) + // Not when another panel is taking over: it has already set the shared + // flag for itself, and clearing it here would leave the incoming panel + // open with the center indicators revealed behind it. + if (!root.popoutSwitchClosing) setCenterHoverRevealSuppressed(false) if (root.editingLocation) root.cancelEditingLocation() root.controller.hide() } @@ -42,6 +53,12 @@ Panel { else root.openFromHotkey() } + function switchPanel(direction) { + if (root.bar && typeof root.bar.switchPanelFrom === "function") + return root.bar.switchPanelFrom(root.barIdentity, direction) + return false + } + function setCenterHoverRevealSuppressed(value) { if (root.bar && "centerHoverRevealSuppressed" in root.bar) root.bar.centerHoverRevealSuppressed = value @@ -440,7 +457,7 @@ Panel { KeyboardPanel { id: panel anchorItem: root.anchorItem - owner: root + owner: root.barIdentity bar: root.bar open: root.opened centerOnBar: true diff --git a/test/shell.d/weather-test.sh b/test/shell.d/weather-test.sh index 90365e2d..954cbf41 100644 --- a/test/shell.d/weather-test.sh +++ b/test/shell.d/weather-test.sh @@ -8,6 +8,7 @@ run_node_test <<'JS' const fs = require('fs') const weather = requireFromRoot('shell/plugins/panels/weather/Model.js') const panelSource = fs.readFileSync(root + '/shell/plugins/panels/weather/Panel.qml', 'utf8') +const widgetSource = fs.readFileSync(root + '/shell/plugins/panels/weather/BarWidget.qml', 'utf8') assertDeepEqual( weather.parseWeatherStatus('{"text":"☀","class":"sunny"}'), @@ -115,6 +116,30 @@ assertEqual(weather.currentIcon({ openMeteoWeatherCode: 0, isDay: 0 }, ''), weat assert(weather.iconForOpenMeteoCode(45, true) !== weather.iconForOpenMeteoCode(45, false), 'weather distinguishes nighttime fog from daytime fog') assertEqual(weather.provisionalCurrentIcon({ weatherCode: 113 }, ''), weather.iconForCode(113, false), 'weather uses wttr to fill an empty initial icon') assertEqual(weather.provisionalCurrentIcon({ weatherCode: 113 }, 'night'), 'night', 'weather refresh preserves a resolved day-night icon') +// The bar identifies a panel by the widget in its slot, so the nested panel +// has to present the host widget rather than itself — otherwise the +// open-panel dot never lights and Tab cannot leave the panel. +assert( + panelSource.includes('owner: root.barIdentity'), + 'weather panel gives the bar its host widget as popout identity' +) +assert( + panelSource.includes('switchPanelFrom(root.barIdentity, direction)'), + 'weather panel switches panels as its host widget' +) +assert( + widgetSource.includes('target.hostWidget = root'), + 'weather widget injects itself as the panel host' +) +assert( + widgetSource.includes('readonly property bool popoutSwitchClosing:') && widgetSource.includes('function closeForPopoutSwitch()'), + 'weather widget forwards the popout-switch handshake' +) +assert( + panelSource.includes('if (!root.popoutSwitchClosing) setCenterHoverRevealSuppressed(false)'), + 'weather leaves the shared hover-reveal flag alone when another panel takes over' +) + assert( panelSource.includes('text: root.label || "—"'), 'weather hero and bar use the same resolved icon'