Show weather popup on hover

PopupCard gains a triggerMode property — "click" stays as the default
(focus-grab dismissal), "hover" skips the grab so the cursor can move
freely between the trigger and the card. PopupCard also exposes a
containsMouse alias for widgets that want to coordinate hover state.

Weather widget now shows on hover and hides 220ms after the cursor
leaves both the trigger and the popup. The redundant tooltip is gone —
the popup is the detail view. Click is still wired for refresh
(middle) and notification dump (right).
This commit is contained in:
Ryan Hughes
2026-05-14 02:21:48 -04:00
parent 7425dd74ae
commit bfa8e2234e
2 changed files with 47 additions and 9 deletions
@@ -13,9 +13,13 @@ PopupWindow {
property int contentWidth: 280 property int contentWidth: 280
property int contentHeight: 200 property int contentHeight: 200
property bool open: false property bool open: false
// "click" — uses HyprlandFocusGrab so clicking outside dismisses the popup.
// "hover" — passive overlay; the owning widget controls open via hover.
property string triggerMode: "click"
readonly property var coordinatorKey: owner || root readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property bool containsMouse: cardHover.hovered
function closePopout() { function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout() if (owner && "closePopout" in owner) owner.closePopout()
@@ -37,9 +41,10 @@ PopupWindow {
// Outside-click dismissal via Hyprland's focus grab. While `active`, input // Outside-click dismissal via Hyprland's focus grab. While `active`, input
// is routed only to the listed windows; clicking anywhere else clears the // is routed only to the listed windows; clicking anywhere else clears the
// grab and we close the popup. // grab and we close the popup. Skipped for hover-mode popups so the cursor
// can move freely between the trigger and the popup.
HyprlandFocusGrab { HyprlandFocusGrab {
active: root.open active: root.open && root.triggerMode === "click"
windows: root.anchorWindow ? [root, root.anchorWindow] : [root] windows: root.anchorWindow ? [root, root.anchorWindow] : [root]
onCleared: root.closePopout() onCleared: root.closePopout()
} }
@@ -99,5 +104,9 @@ PopupWindow {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.padding anchors.margins: root.padding
} }
HoverHandler {
id: cardHover
}
} }
} }
@@ -18,8 +18,6 @@ Item {
readonly property string label: bar ? bar.weatherText : "" readonly property string label: bar ? bar.weatherText : ""
readonly property string klass: bar ? bar.weatherClass : "" readonly property string klass: bar ? bar.weatherClass : ""
// Parse the wttr.in single-line report into location + condition halves so
// we can render them with different emphasis.
readonly property string reportLocation: { readonly property string reportLocation: {
var parts = String(fullReport || "").split(":") var parts = String(fullReport || "").split(":")
return parts.length > 1 ? parts[0].trim() : "" return parts.length > 1 ? parts[0].trim() : ""
@@ -38,6 +36,31 @@ Item {
if (!forecastProc.running) forecastProc.running = true if (!forecastProc.running) forecastProc.running = true
} }
// Hover state across the trigger button and the popup.
property bool buttonHovered: false
property bool popupHovered: popup.containsMouse
function showPopup() {
hideTimer.stop()
if (!popupOpen) refresh()
popupOpen = true
}
function scheduleHide() {
hideTimer.restart()
}
Timer {
id: hideTimer
interval: 220
onTriggered: {
if (!root.buttonHovered && !root.popupHovered) root.popupOpen = false
}
}
onButtonHoveredChanged: buttonHovered ? showPopup() : scheduleHide()
onPopupHoveredChanged: popupHovered ? hideTimer.stop() : scheduleHide()
Process { Process {
id: forecastProc id: forecastProc
command: ["bash", "-lc", "curl -fsS --max-time 5 'wttr.in/?T0&format=%l:+%C+%t+%f+wind+%w+%h+humidity' 2>/dev/null"] command: ["bash", "-lc", "curl -fsS --max-time 5 'wttr.in/?T0&format=%l:+%C+%t+%f+wind+%w+%h+humidity' 2>/dev/null"]
@@ -54,22 +77,28 @@ Item {
text: root.label text: root.label
active: root.klass === "active" active: root.klass === "active"
horizontalMargin: 7.5 horizontalMargin: 7.5
tooltipText: root.fullReport || "Weather" // Tooltip suppressed — the popup itself is the detail view.
tooltipText: ""
onPressed: function(b) { onPressed: function(b) {
if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"") if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"")
else { else if (b === Qt.MiddleButton) root.refresh()
root.popupOpen = !root.popupOpen
if (root.popupOpen) root.refresh()
}
} }
} }
HoverHandler {
id: hoverHandler
target: button
onHoveredChanged: root.buttonHovered = hovered
}
Common.PopupCard { Common.PopupCard {
id: popup
anchorItem: button anchorItem: button
owner: root owner: root
bar: root.bar bar: root.bar
open: root.popupOpen open: root.popupOpen
triggerMode: "hover"
contentWidth: 320 contentWidth: 320
contentHeight: card.implicitHeight + 28 contentHeight: card.implicitHeight + 28