mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-07 22:32:06 -04:00
Initial staging for Animation & Motion effects
This commit is contained in:
@@ -26,10 +26,10 @@ Item {
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantModalExitCurve
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
@@ -44,11 +44,14 @@ Item {
|
||||
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 || useBackground
|
||||
readonly property bool _needsFullscreenMotion: !useSingleWindow && (Theme.isDirectionalEffect || Theme.isDepthEffect)
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
@@ -58,19 +61,34 @@ Item {
|
||||
|
||||
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)
|
||||
if (!useSingleWindow && !_needsFullscreenMotion)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
if (Theme.isDirectionalEffect) {
|
||||
if (!useSingleWindow && !_needsFullscreenMotion)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !_needsFullscreenMotion && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -87,7 +105,7 @@ Item {
|
||||
ModalManager.closeModal(root);
|
||||
closeTimer.stop();
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
if (!useSingleWindow && !_needsFullscreenMotion)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
Qt.callLater(() => animationsEnabled = true);
|
||||
@@ -123,7 +141,7 @@ Item {
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen) {
|
||||
contentWindow.screen = newScreen;
|
||||
if (!useSingleWindow)
|
||||
if (!useSingleWindow && !_needsFullscreenMotion)
|
||||
clickCatcher.screen = newScreen;
|
||||
}
|
||||
}
|
||||
@@ -131,12 +149,12 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
if (!useSingleWindow && !_needsFullscreenMotion)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
}
|
||||
@@ -145,7 +163,17 @@ Item {
|
||||
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: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
||||
readonly property real shadowMotionPadding: {
|
||||
if (_needsFullscreenMotion)
|
||||
return 0;
|
||||
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)
|
||||
@@ -246,19 +274,22 @@ Item {
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
right: root.useSingleWindow || root._needsFullscreenMotion
|
||||
bottom: root.useSingleWindow || root._needsFullscreenMotion
|
||||
}
|
||||
|
||||
readonly property real actualMarginLeft: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
readonly property real actualMarginTop: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
left: actualMarginLeft
|
||||
top: actualMarginTop
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
implicitWidth: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedWidth + (shadowBuffer * 2)
|
||||
implicitHeight: (root.useSingleWindow || root._needsFullscreenMotion) ? 0 : root.alignedHeight + (shadowBuffer * 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
@@ -273,7 +304,7 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
@@ -286,9 +317,9 @@ Item {
|
||||
visible: root.useBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -296,15 +327,15 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
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
|
||||
enabled: (root.useSingleWindow || root._needsFullscreenMotion) && root.shouldBeVisible
|
||||
hoverEnabled: false
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: mouse.accepted = true
|
||||
@@ -313,29 +344,92 @@ Item {
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
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 (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 (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
|
||||
property real scaleValue: root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -343,7 +437,7 @@ Item {
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -351,7 +445,7 @@ Item {
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -367,15 +461,14 @@ Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Item {
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
@@ -23,8 +24,14 @@ Item {
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
// Animation state — matches DankPopout/DankModal pattern
|
||||
property bool animationsEnabled: true
|
||||
property bool _motionActive: false
|
||||
property real _frozenMotionX: 0
|
||||
property real _frozenMotionY: 0
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
@@ -78,6 +85,34 @@ Item {
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
|
||||
// Shadow padding for the content window (render padding only, no motion padding)
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||
|
||||
// For directional/depth: window extends from screen top (content slides within)
|
||||
// For standard: small window tightly around the modal + shadow padding
|
||||
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
|
||||
// Content window geometry
|
||||
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
|
||||
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
|
||||
readonly property real _cwWidth: alignedWidth + shadowPad * 2
|
||||
readonly property real _cwHeight: {
|
||||
if (Theme.isDirectionalEffect)
|
||||
return screenHeight + shadowPad;
|
||||
if (Theme.isDepthEffect)
|
||||
return alignedY + alignedHeight + shadowPad;
|
||||
return alignedHeight + shadowPad * 2;
|
||||
}
|
||||
// Where the content container sits inside the content window
|
||||
readonly property real _ccX: shadowPad
|
||||
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
@@ -97,7 +132,8 @@ Item {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
@@ -130,40 +166,59 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
|
||||
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
backgroundWindow.screen = focusedScreen;
|
||||
contentWindow.screen = focusedScreen;
|
||||
}
|
||||
|
||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
// Make windows visible but do NOT request keyboard focus yet
|
||||
ModalManager.openModal(root);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", "");
|
||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
|
||||
// Frame 1: enable animations and trigger enter motion
|
||||
Qt.callLater(() => {
|
||||
root.animationsEnabled = true;
|
||||
root._motionActive = true;
|
||||
|
||||
// Frame 2: request keyboard focus + activate search field
|
||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
||||
Qt.callLater(() => {
|
||||
root.keyboardActive = true;
|
||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
||||
root.spotlightContent.searchField.forceActiveFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function show() {
|
||||
_openCommon("", "");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query, "");
|
||||
_openCommon(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -171,13 +226,17 @@ Item {
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||
if (!Theme.isDirectionalEffect)
|
||||
contentVisible = false;
|
||||
|
||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
@@ -186,21 +245,7 @@ Item {
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", mode);
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
@@ -221,10 +266,13 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
@@ -242,7 +290,7 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
@@ -267,7 +315,7 @@ Item {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screen = launcherWindow.screen;
|
||||
const screen = contentWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
@@ -289,35 +337,24 @@ Item {
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
launcherWindow.screen = newScreen;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
||||
id: backgroundWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
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.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
@@ -327,11 +364,11 @@ Item {
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
id: bgFullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
@@ -339,13 +376,14 @@ Item {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: launcherMotionVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,49 +391,136 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankLauncherV2Modal: '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: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._cwMarginLeft
|
||||
top: root._cwMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._cwWidth
|
||||
implicitHeight: root._cwHeight
|
||||
|
||||
mask: Region {
|
||||
item: contentInputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
id: contentInputMask
|
||||
visible: false
|
||||
x: contentContainer.x + contentWrapper.x
|
||||
y: contentContainer.y + contentWrapper.y
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
Behavior on opacity {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
||||
x: root._ccX
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
|
||||
readonly property real collapsedMotionY: {
|
||||
if (directionalEffect)
|
||||
return Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1);
|
||||
if (depthEffect)
|
||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// animX/animY are Behavior-animated — DankPopout pattern
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
|
||||
|
||||
Component.onCompleted: {
|
||||
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
|
||||
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
|
||||
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function on_MotionActiveChanged() {
|
||||
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
|
||||
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
anchors.fill: parent
|
||||
level: Theme.elevationLevel3
|
||||
fallbackOffset: 6
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
@@ -403,36 +528,56 @@ Item {
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
@@ -462,7 +462,7 @@ FocusScope {
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
@@ -548,7 +548,6 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -697,7 +696,13 @@ FocusScope {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||
opacity: {
|
||||
if (!root.parentModal)
|
||||
return 1;
|
||||
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
||||
return 1;
|
||||
return root.parentModal.isClosing ? 0 : 1;
|
||||
}
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
|
||||
Reference in New Issue
Block a user