1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/quickshell/Common/DankModalWindow.qml
2025-12-02 15:01:23 -05:00

397 lines
14 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Services
import qs.Widgets
Singleton {
id: root
property var activeModal: null
property bool windowsVisible: false
property var targetScreen: null
property var persistentModal: null
readonly property bool hasActiveModal: activeModal !== null
readonly property bool hasPersistentModal: persistentModal !== null
readonly property bool isPersistentModalActive: hasActiveModal && activeModal === persistentModal
readonly property bool shouldShowModal: hasActiveModal
readonly property bool shouldKeepWindowsAlive: hasPersistentModal
onPersistentModalChanged: {
if (!persistentModal)
return;
cachedModal = persistentModal;
cachedModalWidth = Theme.px(persistentModal.modalWidth, dpr);
cachedModalHeight = Theme.px(persistentModal.modalHeight, dpr);
cachedModalX = calculateX(persistentModal);
cachedModalY = calculateY(persistentModal);
cachedAnimationDuration = persistentModal.animationDuration ?? Theme.shortDuration;
cachedEnterCurve = persistentModal.animationEnterCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedExitCurve = persistentModal.animationExitCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedScaleCollapsed = persistentModal.animationScaleCollapsed ?? 0.96;
if (persistentModal.directContent) {
persistentModal.directContent.parent = directContentWrapper;
persistentModal.directContent.anchors.fill = directContentWrapper;
}
}
readonly property var screen: backgroundWindow.screen
readonly property real dpr: screen ? CompositorService.getScreenScale(screen) : 1
readonly property real shadowBuffer: 5
property bool wantsToHide: false
property real cachedModalWidth: 400
property real cachedModalHeight: 300
property real cachedModalX: 0
property real cachedModalY: 0
property var cachedModal: null
property int cachedAnimationDuration: Theme.shortDuration
property var cachedEnterCurve: Theme.expressiveCurves.expressiveFastSpatial
property var cachedExitCurve: Theme.expressiveCurves.expressiveFastSpatial
property real cachedScaleCollapsed: 0.96
readonly property real modalWidth: cachedModalWidth
readonly property real modalHeight: cachedModalHeight
readonly property real modalX: cachedModalX
readonly property real modalY: cachedModalY
Connections {
target: root.cachedModal
function onModalWidthChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalWidth = Theme.px(root.cachedModal.modalWidth, root.dpr);
root.cachedModalX = root.calculateX(root.cachedModal);
}
function onModalHeightChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalHeight = Theme.px(root.cachedModal.modalHeight, root.dpr);
root.cachedModalY = root.calculateY(root.cachedModal);
}
}
onScreenChanged: {
if (!cachedModal || !screen)
return;
cachedModalWidth = Theme.px(cachedModal.modalWidth, dpr);
cachedModalHeight = Theme.px(cachedModal.modalHeight, dpr);
cachedModalX = calculateX(cachedModal);
cachedModalY = calculateY(cachedModal);
}
function showModal(modal) {
wantsToHide = false;
targetScreen = CompositorService.focusedScreen;
activeModal = modal;
cachedModal = modal;
windowsVisible = true;
cachedModalWidth = Theme.px(modal.modalWidth, dpr);
cachedModalHeight = Theme.px(modal.modalHeight, dpr);
cachedModalX = calculateX(modal);
cachedModalY = calculateY(modal);
cachedAnimationDuration = modal.animationDuration ?? Theme.shortDuration;
cachedEnterCurve = modal.animationEnterCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedExitCurve = modal.animationExitCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedScaleCollapsed = modal.animationScaleCollapsed ?? 0.96;
if (modal.directContent)
Qt.callLater(focusDirectContent);
}
function focusDirectContent() {
if (!hasActiveModal)
return;
if (!cachedModal?.directContent)
return;
cachedModal.directContent.forceActiveFocus();
}
function hideModal() {
wantsToHide = true;
Qt.callLater(completeHide);
}
function completeHide() {
if (!wantsToHide)
return;
activeModal = null;
wantsToHide = false;
}
function hideModalInstant() {
wantsToHide = false;
activeModal = null;
windowsVisible = false;
cleanupInputMethod();
}
function onCloseAnimationFinished() {
if (hasActiveModal)
return;
if (cachedModal && typeof cachedModal.onFullyClosed === "function")
cachedModal.onFullyClosed();
cleanupInputMethod();
windowsVisible = false;
}
function cleanupInputMethod() {
if (!Qt.inputMethod)
return;
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
function calculateX(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const w = Theme.px(m.modalWidth, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.width - w) / 2, dpr);
case "top-right":
return Theme.snap(Math.max(Theme.spacingL, screen.width - w - Theme.spacingL), dpr);
case "custom":
return Theme.snap(m.customPosition.x, dpr);
default:
return 0;
}
}
function calculateY(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const h = Theme.px(m.modalHeight, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.height - h) / 2, dpr);
case "top-right":
return Theme.snap(Theme.barHeight + Theme.spacingXS, dpr);
case "custom":
return Theme.snap(m.customPosition.y, dpr);
default:
return 0;
}
}
PanelWindow {
id: backgroundWindow
visible: root.windowsVisible || root.shouldKeepWindowsAlive
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: "dms:modal:background"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: backgroundMaskRect
intersection: Intersection.Xor
}
Item {
id: backgroundMaskRect
x: root.shouldShowModal ? root.modalX : 0
y: root.shouldShowModal ? root.modalY : 0
width: root.shouldShowModal ? root.modalWidth : (backgroundWindow.screen?.width ?? 1920)
height: root.shouldShowModal ? root.modalHeight : (backgroundWindow.screen?.height ?? 1080)
}
MouseArea {
anchors.fill: parent
enabled: root.windowsVisible
onClicked: mouse => {
if (!root.cachedModal || !root.shouldShowModal)
return;
if (!(root.cachedModal.closeOnBackgroundClick ?? true))
return;
const outside = mouse.x < root.modalX || mouse.x > root.modalX + root.modalWidth || mouse.y < root.modalY || mouse.y > root.modalY + root.modalHeight;
if (!outside)
return;
root.cachedModal.backgroundClicked();
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: root.shouldShowModal && SettingsData.modalDarkenBackground ? (root.cachedModal?.backgroundOpacity ?? 0.5) : 0
visible: SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: root.windowsVisible || root.shouldKeepWindowsAlive
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: root.cachedModal?.layerNamespace ?? "dms:modal"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!root.hasActiveModal)
return WlrKeyboardFocus.None;
if (root.cachedModal?.customKeyboardFocus !== null && root.cachedModal?.customKeyboardFocus !== undefined)
return root.cachedModal.customKeyboardFocus;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.modalX - root.shadowBuffer, root.dpr))
top: Math.max(0, Theme.snap(root.modalY - root.shadowBuffer, root.dpr))
}
implicitWidth: root.modalWidth + (root.shadowBuffer * 2)
implicitHeight: root.modalHeight + (root.shadowBuffer * 2)
mask: Region {
item: contentMaskRect
}
Item {
id: contentMaskRect
x: root.shadowBuffer
y: root.shadowBuffer
width: root.shouldShowModal ? root.modalWidth : 0
height: root.shouldShowModal ? root.modalHeight : 0
}
HyprlandFocusGrab {
windows: [contentWindow]
active: CompositorService.isHyprland && root.hasActiveModal && (root.cachedModal?.shouldHaveFocus ?? false)
}
Item {
id: contentContainer
x: root.shadowBuffer
y: root.shadowBuffer
width: root.modalWidth
height: root.modalHeight
readonly property bool hasDirectContent: root.cachedModal ? (root.cachedModal.directContent !== null && root.cachedModal.directContent !== undefined) : false
opacity: root.shouldShowModal ? 1 : 0
scale: root.shouldShowModal ? 1 : root.cachedScaleCollapsed
onHasDirectContentChanged: {
if (!hasDirectContent)
return;
const dc = root.cachedModal.directContent;
if (dc.parent === directContentWrapper)
return;
dc.parent = directContentWrapper;
dc.anchors.fill = directContentWrapper;
}
Behavior on opacity {
NumberAnimation {
id: opacityAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
onRunningChanged: {
if (running || root.shouldShowModal)
return;
root.onCloseAnimationFinished();
}
}
}
Behavior on scale {
NumberAnimation {
id: scaleAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.cachedModal?.backgroundColor ?? Theme.surfaceContainer
borderColor: root.cachedModal?.borderColor ?? Theme.outlineMedium
borderWidth: root.cachedModal?.borderWidth ?? 1
radius: root.cachedModal?.cornerRadius ?? Theme.cornerRadius
z: -1
}
FocusScope {
id: modalFocusScope
anchors.fill: parent
focus: root.hasActiveModal
Keys.onEscapePressed: event => {
if (!root.cachedModal?.closeOnEscapeKey)
return;
root.cachedModal.close();
event.accepted = true;
}
Keys.forwardTo: contentContainer.hasDirectContent ? [directContentWrapper] : (contentLoader.item ? [contentLoader.item] : [])
Item {
id: directContentWrapper
anchors.fill: parent
visible: contentContainer.hasDirectContent
focus: contentContainer.hasDirectContent && root.hasActiveModal
}
Loader {
id: contentLoader
anchors.fill: parent
active: !contentContainer.hasDirectContent && root.windowsVisible
asynchronous: false
sourceComponent: root.cachedModal?.content ?? null
visible: !contentContainer.hasDirectContent
focus: !contentContainer.hasDirectContent && root.hasActiveModal
onLoaded: {
if (!item)
return;
if (root.cachedModal)
root.cachedModal.loadedContent = item;
if (root.hasActiveModal)
item.forceActiveFocus();
}
onActiveChanged: {
if (active || !root.cachedModal)
return;
root.cachedModal.loadedContent = null;
}
}
}
}
}
}