From d9da88ceb5acfdbaadb4ddb916b9214aac276b5f Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 20 Nov 2025 16:28:26 -0500 Subject: [PATCH] niri: embed spotlight to same window as overview layer --- .../WorkspaceOverlays/NiriOverviewOverlay.qml | 212 +++++++++++++----- 1 file changed, 150 insertions(+), 62 deletions(-) diff --git a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml index d4c473b1..cb718f93 100644 --- a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml +++ b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml @@ -3,26 +3,47 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Common +import qs.Modals.Spotlight import qs.Services Scope { id: niriOverviewScope - // Only show overlay when in overview and spotlight is not open + property bool searchActive: false + property string searchActiveScreen: "" property bool overlayActive: NiriService.inOverview && !(PopoutService.spotlightModal?.spotlightOpen ?? false) + function showSpotlight(screenName) { + searchActive = true + searchActiveScreen = screenName + } + + function hideSpotlight() { + searchActive = false + searchActiveScreen = "" + } + Connections { target: NiriService function onInOverviewChanged() { - if (!NiriService.inOverview && PopoutService.spotlightModal?.openedFromOverview) { - PopoutService.spotlightModal.hide() + if (!NiriService.inOverview) { + hideSpotlight() + } else { + searchActive = false + searchActiveScreen = "" + } + } + + function onCurrentOutputChanged() { + if (NiriService.inOverview && searchActive && searchActiveScreen !== "" && searchActiveScreen !== NiriService.currentOutput) { + hideSpotlight() } } } Loader { id: niriOverlayLoader - active: NiriService.inOverview + active: overlayActive asynchronous: false sourceComponent: Variants { @@ -33,92 +54,159 @@ Scope { id: overlayWindow required property var modelData + readonly property real dpr: CompositorService.getScreenScale(screen) + readonly property bool isActiveScreen: screen.name === NiriService.currentOutput + readonly property bool shouldShowSpotlight: niriOverviewScope.searchActive && screen.name === niriOverviewScope.searchActiveScreen + screen: modelData - visible: niriOverviewScope.overlayActive + visible: NiriService.inOverview color: "transparent" - WlrLayershell.namespace: "dms:niri-overview-overlay" + WlrLayershell.namespace: "dms:niri-overview-spotlight" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: niriOverviewScope.overlayActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: { + if (!NiriService.inOverview) return WlrKeyboardFocus.None + if (!isActiveScreen) return WlrKeyboardFocus.None + return WlrKeyboardFocus.Exclusive + } - implicitWidth: 0 - implicitHeight: 0 + onShouldShowSpotlightChanged: { + if (!shouldShowSpotlight && isActiveScreen) { + Qt.callLater(() => keyboardFocusScope.forceActiveFocus()) + } + } anchors { top: true left: true - right: false - bottom: false + right: true + bottom: true } + FocusScope { id: keyboardFocusScope anchors.fill: parent - visible: niriOverviewScope.overlayActive - focus: niriOverviewScope.overlayActive + focus: true Keys.onPressed: event => { + if (!overlayWindow.shouldShowSpotlight) { + if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) { + NiriService.toggleOverview() + event.accepted = true + return + } - // Handle arrow keys and escape for navigation, mimicking niri's harcoded keybinds - if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) { - NiriService.toggleOverview() - event.accepted = true - return - } + if (event.key === Qt.Key_Left) { + NiriService.moveColumnLeft() + event.accepted = true + return + } - if (event.key === Qt.Key_Left) { - NiriService.moveColumnLeft() - event.accepted = true - return - } + if (event.key === Qt.Key_Right) { + NiriService.moveColumnRight() + event.accepted = true + return + } - if (event.key === Qt.Key_Right) { - NiriService.moveColumnRight() - event.accepted = true - return - } + if (event.key === Qt.Key_Up) { + NiriService.moveWorkspaceUp() + event.accepted = true + return + } - if (event.key === Qt.Key_Up) { - NiriService.moveWorkspaceUp() - event.accepted = true - return - } + if (event.key === Qt.Key_Down) { + NiriService.moveWorkspaceDown() + event.accepted = true + return + } - if (event.key === Qt.Key_Down) { - NiriService.moveWorkspaceDown() - event.accepted = true - return - } + if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) { + event.accepted = false + return + } - // Allowing delete and backspace will produce a broken text - if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) { - event.accepted = false - return - } - - // For any other key (printable characters), open spotlight - if (!event.isAutoRepeat) { - Qt.callLater(() => { - if (PopoutService.spotlightModal) { - if (event.text) { - PopoutService.spotlightModal.openedFromOverview = true - PopoutService.spotlightModal.showWithQuery(event.text.trim()) - } - } - }) - - event.accepted = true - } + if (!event.isAutoRepeat && event.text) { + niriOverviewScope.showSpotlight(overlayWindow.screen.name) + if (spotlightContent?.searchField) { + spotlightContent.searchField.text = event.text.trim() + if (spotlightContent.appLauncher) { + spotlightContent.appLauncher.searchQuery = event.text.trim() } + Qt.callLater(() => spotlightContent.searchField.forceActiveFocus()) + } + event.accepted = true + } + } + } + } + + Item { + id: spotlightContainer + x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr) + y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr) + width: Theme.px(500, overlayWindow.dpr) + height: Theme.px(600, overlayWindow.dpr) + + property real scaleValue: 0.96 + + scale: scaleValue + opacity: overlayWindow.shouldShowSpotlight ? 1 : 0 + + layer.enabled: true + layer.smooth: false + layer.textureSize: Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) Connections { - target: niriOverviewScope - function onOverlayActiveChanged() { - if (niriOverviewScope.overlayActive) { - Qt.callLater(() => keyboardFocusScope.forceActiveFocus()) + target: overlayWindow + function onShouldShowSpotlightChanged() { + spotlightContainer.scaleValue = overlayWindow.shouldShowSpotlight ? 1.0 : 0.96 + } + } + + Behavior on scaleValue { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: niriOverviewScope.searchActive ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: niriOverviewScope.searchActive ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized + } + } + + Rectangle { + anchors.fill: parent + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + } + + SpotlightContent { + id: spotlightContent + anchors.fill: parent + anchors.margins: 0 + + property var fakeParentModal: QtObject { + property bool spotlightOpen: overlayWindow.shouldShowSpotlight + function hide() { + niriOverviewScope.hideSpotlight() + if (overlayWindow.isActiveScreen) { + Qt.callLater(() => keyboardFocusScope.forceActiveFocus()) + } } } + + Component.onCompleted: { + parentModal = fakeParentModal + } } } }