pragma ComponentBehavior: Bound import QtQuick import QtQuick.Window import Quickshell import Quickshell.Wayland PanelWindow { id: root required property Theme theme required property SurfaceManager surfaces required property Niri niriService required property Time timeService property CalDav caldavService required property Resources resourceService required property Notifications notificationService required property Tray trayService required property Media mediaService required property Clipboard clipboardService required property Vault vaultService required property Launcher launcherService required property Audio audioService required property Brightness brightnessService required property Network networkService required property Bluetooth bluetoothService required property Power powerService required property Privilege privilegeService required property Lock lockService required property Noko nokoService required property var barWindow readonly property var contentSize: surfaces ? surfaces.popoutContentSize(surfaces.popoutName) : ({ "width": 320, "height": 180 }) readonly property var popupAnchorRect: surfaces ? surfaces.popupWindowAnchorRect() : ({ "x": 0, "y": 0, "width": 320, "height": 1 }) readonly property var cardFrame: surfaces ? surfaces.popupCardFrame(surfaces.popoutProgress) : ({ "currentRect": { "x": 8, "y": 8, "width": 304, "height": 164 }, "opacity": 1 }) readonly property var currentRect: cardFrame.currentRect property bool awaitingScrollRestore: false readonly property string activeName: surfaces ? surfaces.popoutName : "" readonly property string removingName: surfaces ? surfaces.removingLoaderName : "" readonly property bool vertical: surfaces && surfaces.orientation === "vertical" // subtract the bar on whichever axis it occupies (top for horizontal, left // for vertical) so the popout surface covers only the free region. readonly property int surfaceWidth: surfaces && surfaces.screenWidth > 0 ? (root.vertical ? Math.max(1, surfaces.screenWidth - surfaces.barHeight) : surfaces.screenWidth) : contentSize.width readonly property int surfaceHeight: surfaces && surfaces.screenHeight > 0 ? (root.vertical ? surfaces.screenHeight : Math.max(1, surfaces.screenHeight - surfaces.barHeight)) : contentSize.height function panelScroll(item: var): Flickable { return item ? (item.scrollView || (item as Flickable)) : null; } function savePanelState(name) { const state = root.surfaces.panelStates[name]; const item = root.panelScroll(activeLoader.item); if (!state || !item) return; const audio = activeLoader.item as AudioPanel; if (audio) { state.expandedSink = audio.expandedSink; state.expandedSource = audio.expandedSource; } const network = activeLoader.item as ConnectivityPanel; if (network) state.expandedSsid = network.expandedSsid; state.scrollY = item.contentY; } function restorePanelState() { const name = root.activeName; const item = root.panelScroll(activeLoader.item); const state = root.surfaces.panelStates[name]; if (!item || !state) return; const audio = activeLoader.item as AudioPanel; if (audio) { audio.expandedSink = state.expandedSink; audio.expandedSource = state.expandedSource; } const network = activeLoader.item as ConnectivityPanel; if (network) network.expandedSsid = state.expandedSsid; item.contentY = Math.max(0, Math.min(state.scrollY, item.contentHeight - item.height)); } Connections { target: root.resourceService function onAgentDispatched(): void { if (root.surfaces && root.surfaces.popoutName === "system") root.surfaces.closePopout("agent-dispatched"); } } Connections { target: root.surfaces function onDismissing(name: string): void { root.savePanelState(name); } } Connections { target: root.contentItem.Window.window enabled: root.awaitingScrollRestore function onFrameSwapped() { root.restorePanelState(); root.awaitingScrollRestore = false; } } function screenByName(name) { const want = String(name || ""); const screens = Quickshell.screens; for (let i = 0; i < screens.length; i++) if (String(screens[i].name) === want) return screens[i]; return root.barWindow ? root.barWindow.screen : null; } function contentComponent(name) { switch (String(name)) { case "clock": return clockContent; case "system": return systemContent; case "notifications": return notificationsContent; case "tray": return trayContent; case "media": return mediaContent; case "noko": return nokoContent; case "clipboard": return clipboardContent; case "vault": return vaultContent; case "launcher": return launcherContent; case "audio": return audioContent; case "brightness": return brightnessContent; case "connectivity": return connectivityContent; case "power": return powerContent; default: return helpContent; } } visible: surfaces && (surfaces.popoutOpen || surfaces.popoutClosing) screen: root.screenByName(root.surfaces ? root.surfaces.popoutOutput : "") implicitWidth: root.surfaceWidth implicitHeight: root.surfaceHeight color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "nuguland-popout" WlrLayershell.keyboardFocus: root.visible && (!root.privilegeService || !root.privilegeService.active) ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None onVisibleChanged: { if (!visible && root.surfaces && root.surfaces.popoutOpen && !root.surfaces.popoutClosing) root.surfaces.closePopout("focus-grab-outside"); } onClosed: { if (root.surfaces) root.surfaces.closePopout("popup-closed"); } anchors { top: true left: true right: true bottom: true } Shortcut { sequence: "Escape" context: Qt.WindowShortcut onActivated: { if (root.surfaces) root.surfaces.closePopout("escape-key"); } } Item { id: popoutInputRegion // origin sits just past the bar: below it (horizontal) or right of it // (vertical), so clicks on the bar itself are never captured here. x: root.vertical && root.surfaces ? root.surfaces.barHeight : 0 y: root.vertical || !root.surfaces ? 0 : root.surfaces.barHeight width: Math.max(1, parent.width - x) height: Math.max(1, parent.height - y) } MouseArea { id: outsideDismissLayer x: popoutInputRegion.x y: popoutInputRegion.y width: popoutInputRegion.width height: popoutInputRegion.height acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: mouse => { // mouse.x/y are layer-local; the card rect is in root coords. The // layer origin is offset on both axes (below the top bar, or right // of the left bar), so convert both before the hit test. const rootX = mouse.x + outsideDismissLayer.x; const rootY = mouse.y + outsideDismissLayer.y; const inside = rootX >= card.x && rootX <= card.x + card.width && rootY >= card.y && rootY <= card.y + card.height; if (!inside && root.surfaces) { mouse.accepted = true; root.surfaces.closePopout("outside"); } } } PopupSurface { id: card theme: root.theme padding: 0 x: Math.round(root.currentRect.x) y: Math.round(root.currentRect.y) width: Math.round(root.currentRect.width) height: Math.round(root.currentRect.height) opacity: root.cardFrame.opacity transformOrigin: Item.Top focus: root.visible onVisibleChanged: { if (visible) forceActiveFocus(); } Keys.onEscapePressed: event => { event.accepted = true; if (root.surfaces) root.surfaces.closePopout("escape-key"); } Column { anchors.fill: parent anchors.margins: 16 spacing: 10 Item { id: loaderStack width: parent.width height: parent.height - y clip: true Loader { id: removingLoader anchors.fill: parent active: root.removingName.length > 0 opacity: active ? Math.max(0, 1 - (root.surfaces ? root.surfaces.popoutProgress : 1)) : 0 sourceComponent: active ? root.contentComponent(root.removingName) : null Behavior on opacity { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 180 easing.type: Easing.OutCubic } } } Loader { id: activeLoader anchors.fill: parent opacity: root.surfaces ? root.surfaces.popoutProgress : 1 sourceComponent: root.activeName.length > 0 ? root.contentComponent(root.activeName) : null onLoaded: { root.awaitingScrollRestore = true; Qt.callLater(root.restorePanelState); } Behavior on opacity { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 180 easing.type: Easing.OutCubic } } } } } Behavior on y { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 130 easing.type: Easing.InOutCubic } } Behavior on opacity { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 90 easing.type: Easing.InOutCubic } } } Component { id: clockContent ClockPanel { theme: root.theme timeService: root.timeService caldavService: root.caldavService width: parent ? parent.width : 0 } } Component { id: systemContent SystemPanel { theme: root.theme resourceService: root.resourceService width: parent ? parent.width : 0 } } Component { id: notificationsContent NotificationGroup { theme: root.theme notificationService: root.notificationService width: parent ? parent.width : 0 } } Component { id: trayContent TrayPanel { theme: root.theme trayService: root.trayService width: parent ? parent.width : 0 } } Component { id: mediaContent MediaPanel { theme: root.theme mediaService: root.mediaService width: parent ? parent.width : 0 } } Component { id: nokoContent NokoPanel { theme: root.theme panelState: root.nokoService } } Component { id: clipboardContent ClipboardPanel { theme: root.theme clipboardService: root.clipboardService sessionState: root.surfaces.panelStates.clipboard onPasteRequested: id => root.clipboardService.paste(id) width: parent ? parent.width : 0 } } Component { id: vaultContent VaultPanel { theme: root.theme vaultService: root.vaultService width: parent ? parent.width : 0 height: parent ? parent.height : 0 } } Component { id: launcherContent LauncherPanel { theme: root.theme launcherService: root.launcherService sessionState: root.surfaces.panelStates.launcher surfaces: root.surfaces width: parent ? parent.width : 0 } } Component { id: audioContent AudioPanel { theme: root.theme audioService: root.audioService width: parent ? parent.width : 0 } } Component { id: brightnessContent BrightnessPanel { theme: root.theme brightnessService: root.brightnessService width: parent ? parent.width : 0 } } Component { id: connectivityContent ConnectivityPanel { theme: root.theme resourceService: root.resourceService networkService: root.networkService bluetoothService: root.bluetoothService width: parent ? parent.width : 0 } } Component { id: powerContent PowerPanel { theme: root.theme powerService: root.powerService width: parent ? parent.width : 0 } } Component { id: helpContent Column { spacing: 8 width: parent ? parent.width : 0 NuguText { theme: root.theme text: "no popout content." width: parent.width wrapMode: Text.WordWrap } } } mask: Region { item: popoutInputRegion } Behavior on implicitWidth { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 180 easing.type: Easing.OutCubic } } Behavior on implicitHeight { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 180 easing.type: Easing.OutCubic } } }