import QtQuick import Quickshell import Quickshell.Wayland import qs.Common import qs.Services import qs.Widgets Item { id: root property string layerNamespace: "dms:modal" property alias content: contentLoader.sourceComponent property alias contentLoader: contentLoader property Item directContent: null property real modalWidth: 400 property real modalHeight: 300 property var targetScreen readonly property var effectiveScreen: contentWindow.screen ?? targetScreen readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 property bool showBackground: true property real backgroundOpacity: 0.5 property string positioning: "center" property point customPosition: Qt.point(0, 0) property bool closeOnEscapeKey: true property bool closeOnBackgroundClick: true property string animationType: "scale" property int animationDuration: Theme.modalAnimationDuration property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationOffset: Theme.effectAnimOffset property list animationEnterCurve: Theme.variantModalEnterCurve property list animationExitCurve: Theme.variantModalExitCurve property color backgroundColor: Theme.surfaceContainer property color borderColor: Theme.outlineMedium property real borderWidth: 0 property real cornerRadius: Theme.cornerRadius readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled property bool enableShadow: true property alias modalFocusScope: focusScope property bool shouldBeVisible: false property bool shouldHaveFocus: shouldBeVisible property bool allowFocusOverride: false property bool allowStacking: false property bool keepContentLoaded: false property bool keepPopoutsOpen: false property var customKeyboardFocus: null property bool useOverlayLayer: false property real frozenMotionOffsetX: 0 property real frozenMotionOffsetY: 0 readonly property alias contentWindow: contentWindow readonly property alias clickCatcher: clickCatcher readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useSingleWindow: CompositorService.isHyprland signal opened signal dialogClosed signal backgroundClicked property bool animationsEnabled: true function open() { closeTimer.stop(); animationsEnabled = false; frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0; frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset; const focusedScreen = CompositorService.getFocusedScreen(); if (focusedScreen) { contentWindow.screen = focusedScreen; if (!useSingleWindow) clickCatcher.screen = focusedScreen; } if (Theme.isDirectionalEffect || root.useBackground) { if (!useSingleWindow) clickCatcher.visible = true; contentWindow.visible = true; } ModalManager.openModal(root); Qt.callLater(() => { animationsEnabled = true; shouldBeVisible = true; if (!useSingleWindow && !clickCatcher.visible) clickCatcher.visible = true; if (!contentWindow.visible) contentWindow.visible = true; shouldHaveFocus = false; Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); }); } function close() { shouldBeVisible = false; shouldHaveFocus = false; ModalManager.closeModal(root); closeTimer.restart(); } function instantClose() { animationsEnabled = false; shouldBeVisible = false; shouldHaveFocus = false; ModalManager.closeModal(root); closeTimer.stop(); contentWindow.visible = false; if (!useSingleWindow) clickCatcher.visible = false; dialogClosed(); Qt.callLater(() => animationsEnabled = true); } function toggle() { shouldBeVisible ? close() : open(); } Connections { target: ModalManager function onCloseAllModalsExcept(excludedModal) { if (excludedModal !== root && !allowStacking && shouldBeVisible) close(); } } Connections { target: Quickshell function onScreensChanged() { if (!contentWindow.screen) return; const currentScreenName = contentWindow.screen.name; let screenStillExists = false; for (let i = 0; i < Quickshell.screens.length; i++) { if (Quickshell.screens[i].name === currentScreenName) { screenStillExists = true; break; } } if (screenStillExists) return; const newScreen = CompositorService.getFocusedScreen(); if (newScreen) { contentWindow.screen = newScreen; if (!useSingleWindow) clickCatcher.screen = newScreen; } } } Timer { id: closeTimer interval: Theme.variantCloseInterval(animationDuration) onTriggered: { if (shouldBeVisible) return; contentWindow.visible = false; if (!useSingleWindow) clickCatcher.visible = false; dialogClosed(); } } readonly property var shadowLevel: Theme.elevationLevel3 readonly property real shadowFallbackOffset: 6 readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowMotionPadding: { if (Theme.isConnectedEffect) return 0; if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect) return 0; // Wayland native overlap mask if (animationType === "slide") return 30; if (Theme.isDirectionalEffect) return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9); if (Theme.isDepthEffect) return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35); return Math.max(0, animationOffset); } readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedX: Theme.snap((() => { switch (positioning) { case "center": return (screenWidth - alignedWidth) / 2; case "top-right": return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL); case "custom": return customPosition.x; default: return 0; } })(), dpr) readonly property real alignedY: Theme.snap((() => { switch (positioning) { case "center": return (screenHeight - alignedHeight) / 2; case "top-right": return Theme.barHeight + Theme.spacingXS; case "custom": return customPosition.y; default: return 0; } })(), dpr) PanelWindow { id: clickCatcher visible: false color: "transparent" WlrLayershell.namespace: root.layerNamespace + ":clickcatcher" WlrLayershell.layer: WlrLayershell.Top WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { top: true left: true right: true bottom: true } mask: Region { item: Rectangle { x: root.alignedX y: root.alignedY width: root.alignedWidth height: root.alignedHeight } intersection: Intersection.Xor } MouseArea { anchors.fill: parent enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible onClicked: root.backgroundClicked() } Rectangle { anchors.fill: parent z: -1 color: "black" opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 visible: opacity > 0 Behavior on opacity { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } } } PanelWindow { id: contentWindow visible: false color: "transparent" WindowBlur { targetWindow: contentWindow blurEnabled: root.effectiveBlurEnabled readonly property real s: Math.min(1, modalContainer.scaleValue) blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 blurRadius: root.effectiveCornerRadius } WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: { if (root.useOverlayLayer) return WlrLayershell.Overlay; switch (Quickshell.env("DMS_MODAL_LAYER")) { case "bottom": console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); return WlrLayershell.Top; case "background": console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); return WlrLayershell.Top; case "overlay": return WlrLayershell.Overlay; default: return WlrLayershell.Top; } } WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: { if (customKeyboardFocus !== null) return customKeyboardFocus; if (!shouldHaveFocus) return WlrKeyboardFocus.None; if (root.useHyprlandFocusGrab) return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.Exclusive; } anchors { left: true top: true right: root.useSingleWindow bottom: root.useSingleWindow } readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) WlrLayershell.margins { left: actualMarginLeft top: actualMarginTop right: 0 bottom: 0 } implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2) implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2) onVisibleChanged: { if (visible) { opened(); } else { if (Qt.inputMethod) { Qt.inputMethod.hide(); Qt.inputMethod.reset(); } } } MouseArea { anchors.fill: parent enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible z: -2 onClicked: root.backgroundClicked() } Rectangle { anchors.fill: parent z: -1 color: "black" opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 visible: opacity > 0 Behavior on opacity { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } } Item { id: modalContainer x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr) y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr) width: root.alignedWidth height: root.alignedHeight MouseArea { anchors.fill: parent enabled: root.useSingleWindow && root.shouldBeVisible hoverEnabled: false acceptedButtons: Qt.AllButtons onPressed: mouse.accepted = true onClicked: mouse.accepted = true z: -1 } readonly property bool slide: root.animationType === "slide" readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool depthEffect: Theme.isDepthEffect readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8) readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36) readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5 readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5 readonly property real customDistLeft: customAnchorX readonly property real customDistRight: root.screenWidth - customAnchorX readonly property real customDistTop: customAnchorY readonly property real customDistBottom: root.screenHeight - customAnchorY readonly property real offsetX: { if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) return 0; if (slide && !directionalEffect && !depthEffect) return 15; if (directionalEffect) { switch (root.positioning) { case "top-right": return 0; case "custom": if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom) return -directionalTravel; if (customDistRight <= customDistTop && customDistRight <= customDistBottom) return directionalTravel; return 0; default: return 0; } } if (depthEffect) { switch (root.positioning) { case "top-right": return 0; case "custom": if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom) return -depthTravel; if (customDistRight <= customDistTop && customDistRight <= customDistBottom) return depthTravel; return 0; default: return 0; } } return 0; } readonly property real offsetY: { if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) return 0; if (slide && !directionalEffect && !depthEffect) return -30; if (directionalEffect) { switch (root.positioning) { case "top-right": return -Math.max(directionalTravel * 0.65, 96); case "custom": if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight) return -directionalTravel; if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight) return directionalTravel; return 0; default: // Default to sliding down from top when centered return -Math.max(directionalTravel, root.screenHeight * 0.24); } } if (depthEffect) { switch (root.positioning) { case "top-right": return -depthTravel * 0.75; case "custom": if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight) return -depthTravel; if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight) return depthTravel; return depthTravel * 0.45; default: return -depthTravel; } } return root.animationOffset; } property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed Behavior on animX { enabled: root.animationsEnabled NumberAnimation { duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on animY { enabled: root.animationsEnabled NumberAnimation { duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on scaleValue { enabled: root.animationsEnabled NumberAnimation { duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Item { id: contentContainer anchors.centerIn: parent width: parent.width height: parent.height clip: false Item { id: animatedContent anchors.fill: parent clip: false opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0) scale: modalContainer.scaleValue transformOrigin: Item.Center Behavior on opacity { enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } ElevationShadow { id: modalShadowLayer anchors.fill: parent level: root.shadowLevel fallbackOffset: root.shadowFallbackOffset targetRadius: root.effectiveCornerRadius targetColor: root.effectiveBackgroundColor borderColor: root.effectiveBorderColor borderWidth: root.effectiveBorderWidth shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } Rectangle { anchors.fill: parent radius: root.effectiveCornerRadius color: "transparent" border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth z: 100 } FocusScope { anchors.fill: parent focus: root.shouldBeVisible clip: false Item { id: directContentWrapper anchors.fill: parent visible: root.directContent !== null focus: true clip: false Component.onCompleted: { if (root.directContent) { root.directContent.parent = directContentWrapper; root.directContent.anchors.fill = directContentWrapper; Qt.callLater(() => root.directContent.forceActiveFocus()); } } Connections { target: root function onDirectContentChanged() { if (root.directContent) { root.directContent.parent = directContentWrapper; root.directContent.anchors.fill = directContentWrapper; Qt.callLater(() => root.directContent.forceActiveFocus()); } } } } Loader { id: contentLoader anchors.fill: parent active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible) asynchronous: false focus: true clip: false visible: root.directContent === null onLoaded: { if (item) { Qt.callLater(() => item.forceActiveFocus()); } } } } } } } FocusScope { id: focusScope objectName: "modalFocusScope" anchors.fill: parent visible: root.shouldBeVisible || contentWindow.visible focus: root.shouldBeVisible Keys.onEscapePressed: event => { if (root.closeOnEscapeKey && shouldHaveFocus) { root.close(); event.accepted = true; } } } } }