1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-04 11:32:12 -04:00

feat(DMS FrameMode): A New Connected Unified Surface & Animation Overhaul

- Introduces Standalone & Connected Modes
- Updated Animations & Motion effects for both modes
- Numerous QOL tweaks and updates throughout the system
- Highly inspired to the OG Caelestia Shell / @Soramanew
This commit is contained in:
purian23
2026-05-03 15:38:30 -04:00
parent dd668469d7
commit fd24b4a36d
82 changed files with 12026 additions and 2905 deletions

View File

@@ -64,11 +64,19 @@ DankModal {
activeImageLoads = 0;
shouldHaveFocus = true;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();

View File

@@ -53,8 +53,6 @@ DankPopout {
open();
activeImageLoads = 0;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
@@ -121,8 +119,16 @@ DankPopout {
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
return;
if (clipboardAvailable)
ClipboardService.refresh();
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (root.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item?.searchField) {

View File

@@ -26,9 +26,7 @@ Rectangle {
spacing: 2
StyledText {
text: keyboardHints.enterToPaste
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -1,25 +1,18 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankModal")
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
property string layerNamespace: "dms:modal"
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Component content: null
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"
@@ -37,7 +30,6 @@ Item {
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: true
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
@@ -46,452 +38,170 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
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
signal opened
signal dialogClosed
signal backgroundClicked
property bool animationsEnabled: true
readonly property var contentLoader: impl.item ? impl.item.contentLoader : null
readonly property alias modalFocusScope: _modalFocusScope
FocusScope {
id: _modalFocusScope
objectName: "modalFocusScope"
focus: true
anchors.fill: parent
}
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
readonly property var clickCatcher: impl.item ? impl.item.clickCatcher : null
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
readonly property real dpr: impl.item ? impl.item.dpr : 1
readonly property bool isClosing: impl.item ? (impl.item.isClosing ?? false) : false
readonly property real alignedX: impl.item ? impl.item.alignedX : 0
readonly property real alignedY: impl.item ? impl.item.alignedY : 0
readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0
readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0
function open() {
closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) {
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen;
if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen;
}
}
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
function _finishOpen() {
ModalManager.openModal(root);
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
if (impl.item)
impl.item.open();
}
function close() {
shouldBeVisible = false;
shouldHaveFocus = false;
ModalManager.closeModal(root);
closeTimer.restart();
if (impl.item)
impl.item.close();
}
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);
if (impl.item && typeof impl.item.instantClose === "function")
impl.item.instantClose();
}
function toggle() {
shouldBeVisible ? close() : open();
if (impl.item)
impl.item.toggle();
}
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
property var _resolvedBackend: null
Component.onCompleted: _resolvedBackend = _desiredBackend
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend();
}
}
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down a modal mid-animation when frame mode is toggled.
function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend)
return;
if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing))
return;
_resolvedBackend = _desiredBackend;
}
Loader {
id: impl
sourceComponent: root._resolvedBackend
onItemChanged: if (item)
root._wireBackend(item)
}
Component {
id: standaloneComp
DankModalStandalone {}
}
Component {
id: connectedComp
DankModalConnected {}
}
function _wireBackend(it) {
if (!it)
return;
it.modalHandle = root;
it.layerNamespace = Qt.binding(() => root.layerNamespace);
it.content = Qt.binding(() => root.content);
it.directContent = Qt.binding(() => root.directContent);
it.modalWidth = Qt.binding(() => root.modalWidth);
it.modalHeight = Qt.binding(() => root.modalHeight);
it.targetScreen = Qt.binding(() => root.targetScreen);
it.showBackground = Qt.binding(() => root.showBackground);
it.backgroundOpacity = Qt.binding(() => root.backgroundOpacity);
it.positioning = Qt.binding(() => root.positioning);
it.customPosition = Qt.binding(() => root.customPosition);
it.closeOnEscapeKey = Qt.binding(() => root.closeOnEscapeKey);
it.closeOnBackgroundClick = Qt.binding(() => root.closeOnBackgroundClick);
it.animationType = Qt.binding(() => root.animationType);
it.animationDuration = Qt.binding(() => root.animationDuration);
it.animationScaleCollapsed = Qt.binding(() => root.animationScaleCollapsed);
it.animationOffset = Qt.binding(() => root.animationOffset);
it.animationEnterCurve = Qt.binding(() => root.animationEnterCurve);
it.animationExitCurve = Qt.binding(() => root.animationExitCurve);
it.backgroundColor = Qt.binding(() => root.backgroundColor);
it.borderColor = Qt.binding(() => root.borderColor);
it.borderWidth = Qt.binding(() => root.borderWidth);
it.cornerRadius = Qt.binding(() => root.cornerRadius);
it.enableShadow = Qt.binding(() => root.enableShadow);
it.allowFocusOverride = Qt.binding(() => root.allowFocusOverride);
it.allowStacking = Qt.binding(() => root.allowStacking);
it.keepContentLoaded = Qt.binding(() => root.keepContentLoaded);
it.keepPopoutsOpen = Qt.binding(() => root.keepPopoutsOpen);
it.customKeyboardFocus = Qt.binding(() => root.customKeyboardFocus);
it.useOverlayLayer = Qt.binding(() => root.useOverlayLayer);
it.shouldBeVisible = root.shouldBeVisible;
it.shouldHaveFocus = root.shouldHaveFocus;
if (it.modalFocusScope)
_modalFocusScope.parent = it.modalFocusScope;
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible)
close();
target: root
function onShouldBeVisibleChanged() {
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
impl.item.shouldBeVisible = root.shouldBeVisible;
}
function onShouldHaveFocusChanged() {
if (impl.item && impl.item.shouldHaveFocus !== root.shouldHaveFocus)
impl.item.shouldHaveFocus = root.shouldHaveFocus;
}
}
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;
}
}
}
target: impl.item
ignoreUnknownSignals: true
Timer {
id: closeTimer
interval: animationDuration + 50
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: animationType === "slide" ? 30 : 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
function onShouldBeVisibleChanged() {
if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible)
root.shouldBeVisible = impl.item.shouldBeVisible;
}
mask: Region {
item: Rectangle {
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
intersection: Intersection.Xor
function onShouldHaveFocusChanged() {
if (impl.item && root.shouldHaveFocus !== impl.item.shouldHaveFocus)
root.shouldHaveFocus = impl.item.shouldHaveFocus;
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
}
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
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: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
function onOpened() {
root.opened();
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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;
function onDialogClosed() {
root.dialogClosed();
root._maybeResolveBackend();
}
anchors {
left: true
top: true
right: root.useSingleWindow
bottom: root.useSingleWindow
}
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))
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.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
Behavior on opacity {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
Item {
id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
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 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;
}
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
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: 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
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: animationDuration
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.cornerRadius
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: 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;
}
}
function onBackgroundClicked() {
root.backgroundClicked();
}
}
}

View File

@@ -0,0 +1,838 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankModalConnected")
property var modalHandle: 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"
// Opposite side from the launcher by default; subclasses may override
property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
function _dockOccupiesSide(side) {
if (!SettingsData.showDock)
return false;
switch (side) {
case "top":
return SettingsData.dockPosition === SettingsData.Position.Top;
case "bottom":
return SettingsData.dockPosition === SettingsData.Position.Bottom;
case "left":
return SettingsData.dockPosition === SettingsData.Position.Left;
case "right":
return SettingsData.dockPosition === SettingsData.Position.Right;
}
return false;
}
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
readonly property bool connectedMotionParity: Theme.isConnectedEffect
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : 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: false
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
signal backgroundClicked
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer {
id: _syncTimer
interval: 0
onTriggered: root._flushSync()
}
property bool animationsEnabled: true
property string _chromeClaimId: ""
property bool _fullSyncPending: false
function _nextChromeClaimId() {
return layerNamespace + ":modal:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
}
function _currentScreenName() {
return effectiveScreen ? effectiveScreen.name : "";
}
function _publishModalChromeState() {
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalState(screenName, {
"visible": shouldBeVisible || contentWindow.visible,
"barSide": resolvedConnectedBarSide,
"bodyX": alignedX,
"bodyY": alignedY,
"bodyW": alignedWidth,
"bodyH": alignedHeight,
"animX": modalContainer ? modalContainer.animX : 0,
"animY": modalContainer ? modalContainer.animY : 0,
"omitStartConnector": false,
"omitEndConnector": false
});
}
function _syncModalChromeState() {
if (!frameOwnsConnectedChrome) {
_releaseModalChrome();
return;
}
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishModalChromeState();
if (_dockBlocksEmergence && (shouldBeVisible || contentWindow.visible))
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
else
ConnectedModeState.releaseDockRetract(_chromeClaimId);
}
property bool _animSyncQueued: false
property bool _bodySyncQueued: false
function _queueFullSync() {
_fullSyncPending = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueAnimSync() {
_animSyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueBodySync() {
_bodySyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _flushSync() {
const fullDirty = _fullSyncPending;
const animDirty = _animSyncQueued;
const bodyDirty = _bodySyncQueued;
_fullSyncPending = false;
_animSyncQueued = false;
_bodySyncQueued = false;
if (fullDirty)
_syncModalChromeState();
if (animDirty)
_syncModalAnim();
if (bodyDirty)
_syncModalBody();
}
function _syncModalAnim() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
return;
const screenName = _currentScreenName();
if (!screenName || !modalContainer)
return;
ConnectedModeState.setModalAnim(screenName, modalContainer.animX, modalContainer.animY);
}
function _syncModalBody() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
return;
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight);
}
function _releaseModalChrome() {
if (_chromeClaimId) {
ConnectedModeState.releaseDockRetract(_chromeClaimId);
_chromeClaimId = "";
}
const screenName = _currentScreenName();
if (screenName)
ConnectedModeState.clearModalState(screenName);
}
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
onResolvedConnectedBarSideChanged: _queueFullSync()
onShouldBeVisibleChanged: _queueFullSync()
onAlignedXChanged: _queueBodySync()
onAlignedYChanged: _queueBodySync()
onAlignedWidthChanged: _queueBodySync()
onAlignedHeightChanged: _queueBodySync()
Component.onDestruction: _releaseModalChrome()
Connections {
target: contentWindow
function onVisibleChanged() {
if (contentWindow.visible)
root._syncModalChromeState();
else
root._releaseModalChrome();
}
}
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(modalHandle);
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(modalHandle);
closeTimer.restart();
}
function instantClose() {
animationsEnabled = false;
shouldBeVisible = false;
shouldHaveFocus = false;
ModalManager.closeModal(modalHandle);
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 !== modalHandle && !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();
}
}
// shadowRenderPadding is zeroed when frame owns the chrome
// Wayland then clips any content translating past
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && 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 (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)
function _frameEdgeInset(side) {
if (!effectiveScreen)
return 0;
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
// frameEdgeInsetForSide is the full inset; do not add frameBarSize
readonly property real _connectedAlignedX: {
switch (resolvedConnectedBarSide) {
case "top":
case "bottom":
{
const insetL = _frameEdgeInset("left");
const insetR = _frameEdgeInset("right");
const usable = Math.max(0, screenWidth - insetL - insetR);
return insetL + Math.max(0, (usable - alignedWidth) / 2);
}
case "left":
return _frameEdgeInset("left");
case "right":
return screenWidth - alignedWidth - _frameEdgeInset("right");
}
return 0;
}
readonly property real _connectedAlignedY: {
switch (resolvedConnectedBarSide) {
case "top":
return _frameEdgeInset("top");
case "bottom":
return screenHeight - alignedHeight - _frameEdgeInset("bottom");
case "left":
case "right":
{
const insetT = _frameEdgeInset("top");
const insetB = _frameEdgeInset("bottom");
const usable = Math.max(0, screenHeight - insetT - insetB);
return insetT + Math.max(0, (usable - alignedHeight) / 2);
}
}
return 0;
}
readonly property real alignedX: Theme.snap(frameOwnsConnectedChrome ? _connectedAlignedX : (() => {
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(frameOwnsConnectedChrome ? _connectedAlignedY : (() => {
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 && !root.frameOwnsConnectedChrome
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 && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? 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":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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
// Connected emergence: travel from the resolved bar edge, matching DankPopout cadence.
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real offsetX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -connectedEmergenceTravelX;
case "right":
return connectedEmergenceTravelX;
}
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 (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -connectedEmergenceTravelY;
case "bottom":
return connectedEmergenceTravelY;
}
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;
}
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// openProgress: 0 = closed (at frozenMotionOffset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
Item {
id: animatedContent
anchors.fill: parent
clip: false
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
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
}
}
Behavior on publishedOpacity {
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.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
shadowEnabled: !root.frameOwnsConnectedChrome && 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 || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 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;
}
}
}
}
}

View File

@@ -0,0 +1,484 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankModalStandalone")
property var modalHandle: 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: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: true
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool isClosing: 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
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
signal opened
signal dialogClosed
signal backgroundClicked
property bool animationsEnabled: true
function open() {
closeTimer.stop();
isClosing = false;
const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) {
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen;
if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen;
}
}
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
function _finishOpen() {
ModalManager.openModal(modalHandle);
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
}
function close() {
isClosing = true;
shouldBeVisible = false;
shouldHaveFocus = false;
ModalManager.closeModal(modalHandle);
closeTimer.restart();
}
function instantClose() {
animationsEnabled = false;
isClosing = false;
shouldBeVisible = false;
shouldHaveFocus = false;
ModalManager.closeModal(modalHandle);
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 !== modalHandle && !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: animationDuration + 50
onTriggered: {
if (shouldBeVisible)
return;
isClosing = false;
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: animationType === "slide" ? 30 : 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.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
}
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
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: shouldBeVisible ? modalContainer.width * s : 0
blurHeight: shouldBeVisible ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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
}
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))
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.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
easing.type: Easing.BezierSpline
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
Item {
id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
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 real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: modalContainer.offsetX * (1 - morph.openProgress)
readonly property real animY: modalContainer.offsetY * (1 - morph.openProgress)
readonly property real scaleValue: root.animationScaleCollapsed + (1.0 - root.animationScaleCollapsed) * morph.openProgress
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: 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
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: animationDuration
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.cornerRadius
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
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.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: 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;
}
}
}
}
}

View File

@@ -1,466 +1,118 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankLauncherV2Modal")
visible: false
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
property bool _pendingInitialize: false
property string _pendingQuery: ""
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: Theme.cornerRadius
readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium;
switch (SettingsData.dankLauncherV2BorderColor) {
case "primary":
return Theme.primary;
case "secondary":
return Theme.secondary;
case "outline":
return Theme.outline;
case "surfaceText":
return Theme.surfaceText;
default:
return Theme.primary;
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property bool spotlightOpen: impl.item ? impl.item.spotlightOpen : false
readonly property bool isClosing: impl.item ? impl.item.isClosing : false
readonly property bool keyboardActive: impl.item ? impl.item.keyboardActive : false
readonly property bool contentVisible: impl.item ? impl.item.contentVisible : false
readonly property var spotlightContent: impl.item ? impl.item.spotlightContent : null
readonly property bool openedFromOverview: impl.item ? impl.item.openedFromOverview : false
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
readonly property real dpr: impl.item ? impl.item.dpr : 1
readonly property int modalWidth: impl.item ? impl.item.modalWidth : 620
readonly property int modalHeight: impl.item ? impl.item.modalHeight : 600
readonly property real modalX: impl.item ? impl.item.modalX : 0
readonly property real modalY: impl.item ? impl.item.modalY : 0
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
_pendingInitialize = true;
contentVisible = true;
launcherContentLoader.active = true;
if (spotlightContent) {
_initializeAndShow(_pendingQuery, _pendingMode);
_pendingInitialize = false;
}
}
function _initializeAndShow(query, mode) {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.searchQuery = targetQuery;
spotlightContent.controller.performSearch();
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll();
}
if (spotlightContent.actionPanel) {
spotlightContent.actionPanel.hide();
}
}
function _finishShow(query, mode) {
spotlightOpen = true;
isClosing = false;
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query || "", mode || "");
}
function show() {
closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", ""));
return;
}
_finishShow("", "");
if (impl.item)
impl.item.show();
}
function showWithQuery(query) {
closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
_finishShow(query, "");
}
function hide() {
if (!spotlightOpen)
return;
openedFromOverview = false;
isClosing = true;
contentVisible = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(root);
closeCleanupTimer.start();
}
function toggle() {
spotlightOpen ? hide() : show();
if (impl.item)
impl.item.showWithQuery(query);
}
function showWithMode(mode) {
closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", mode));
return;
}
spotlightOpen = true;
isClosing = false;
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
if (impl.item)
impl.item.showWithMode(mode);
}
function toggleWithMode(mode) {
if (spotlightOpen) {
hide();
} else {
showWithMode(mode);
}
function hide() {
if (impl.item)
impl.item.hide();
}
function toggle() {
if (impl.item)
impl.item.toggle();
}
function toggleWithQuery(query) {
if (spotlightOpen) {
hide();
} else {
showWithQuery(query);
if (impl.item)
impl.item.toggleWithQuery(query);
}
function toggleWithMode(mode) {
if (impl.item)
impl.item.toggleWithMode(mode);
}
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
property var _resolvedBackend: null
Component.onCompleted: _resolvedBackend = _desiredBackend
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend();
}
}
Timer {
id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50
repeat: false
onTriggered: {
isClosing = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
}
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down the launcher mid-animation when frame mode is toggled.
function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend)
return;
if (impl.item && (impl.item.spotlightOpen || impl.item.isClosing))
return;
_resolvedBackend = _desiredBackend;
}
Loader {
id: impl
sourceComponent: root._resolvedBackend
onItemChanged: if (item)
root._wireBackend(item)
}
Component {
id: standaloneComp
DankLauncherV2ModalStandalone {}
}
Component {
id: connectedComp
DankLauncherV2ModalConnected {}
}
function _wireBackend(it) {
if (!it)
return;
it.modalHandle = root;
}
Connections {
target: spotlightContent?.controller ?? null
target: impl.item
ignoreUnknownSignals: true
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
}
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
active: false
onCleared: {
if (spotlightOpen) {
hide();
}
}
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && spotlightOpen) {
hide();
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (Quickshell.screens.length === 0)
return;
const screenName = launcherWindow.screen?.name;
if (screenName) {
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName)
return;
}
}
if (spotlightOpen)
hide();
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (newScreen)
launcherWindow.screen = newScreen;
}
}
PanelWindow {
id: launcherWindow
visible: spotlightOpen || isClosing
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.scale)
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.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
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: spotlightOpen ? fullScreenMask : null
}
Item {
id: fullScreenMask
anchors.fill: parent
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0
Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: mouse => {
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
root.hide();
}
}
}
Item {
id: modalContainer
x: root.modalX
y: root.modalY
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
fallbackOffset: 6
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
}
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;
}
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
function onDialogClosed() {
root.dialogClosed();
root._maybeResolveBackend();
}
}
}

View File

@@ -0,0 +1,922 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
property var modalHandle: root
visible: false
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false
property string _pendingQuery: ""
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: 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
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
readonly property string preferredConnectedBarSide: SettingsData.frameLauncherEmergeSide
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
function _dockOccupiesSide(side) {
if (!SettingsData.showDock)
return false;
switch (side) {
case "top":
return SettingsData.dockPosition === SettingsData.Position.Top;
case "bottom":
return SettingsData.dockPosition === SettingsData.Position.Bottom;
case "left":
return SettingsData.dockPosition === SettingsData.Position.Left;
case "right":
return SettingsData.dockPosition === SettingsData.Position.Right;
}
return false;
}
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
function _frameEdgeInset(side) {
if (!effectiveScreen)
return 0;
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
// frameEdgeInsetForSide is the full inset; do not add frameBarSize.
// Positions the modal flush to the emerge side, centered on the cross axis.
readonly property var _connectedModalPos: {
const fallback = {
"x": (screenWidth - modalWidth) / 2,
"y": (screenHeight - modalHeight) / 2
};
switch (resolvedConnectedBarSide) {
case "top":
case "bottom":
{
const insetL = _frameEdgeInset("left");
const insetR = _frameEdgeInset("right");
const insetT = _frameEdgeInset("top");
const insetB = _frameEdgeInset("bottom");
const usable = Math.max(0, screenWidth - insetL - insetR);
const usableH = Math.max(0, screenHeight - insetT - insetB);
return {
"x": insetL + Math.max(0, (usable - modalWidth) / 2),
"y": launcherArcExtenderActive ? insetT + Math.max(0, (usableH - modalHeight) / 2) : (resolvedConnectedBarSide === "top" ? insetT : screenHeight - modalHeight - insetB)
};
}
case "left":
case "right":
{
const insetT = _frameEdgeInset("top");
const insetB = _frameEdgeInset("bottom");
const usable = Math.max(0, screenHeight - insetT - insetB);
return {
"x": resolvedConnectedBarSide === "left" ? _frameEdgeInset("left") : screenWidth - modalWidth - _frameEdgeInset("right"),
"y": insetT + Math.max(0, (usable - modalHeight) / 2)
};
}
}
return fallback;
}
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium;
switch (SettingsData.dankLauncherV2BorderColor) {
case "primary":
return Theme.primary;
case "secondary":
return Theme.secondary;
case "outline":
return Theme.outline;
case "surfaceText":
return Theme.surfaceText;
default:
return Theme.primary;
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
// Shadow padding for the content window (render padding only, no motion padding).
// Zeroed when frame owns the chrome and Wayland clips past the bar edge
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && 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)
readonly property real _connectedChromeX: alignedX
readonly property real _connectedChromeY: {
if (!launcherArcExtenderActive)
return alignedY;
return resolvedConnectedBarSide === "top" ? Theme.snap(_frameEdgeInset("top"), dpr) : alignedY;
}
readonly property real _connectedChromeWidth: alignedWidth
readonly property real _connectedChromeHeight: {
if (!launcherArcExtenderActive)
return alignedHeight;
if (resolvedConnectedBarSide === "top")
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - _frameEdgeInset("top")), dpr);
if (resolvedConnectedBarSide === "bottom")
return Theme.snap(Math.max(alignedHeight, screenHeight - _frameEdgeInset("bottom") - alignedY), dpr);
return alignedHeight;
}
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
// 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.isConnectedEffect) || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: launcherArcExtenderActive ? _connectedChromeY : (_needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr))
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (launcherArcExtenderActive)
return _connectedChromeHeight;
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
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: launcherArcExtenderActive ? 0 : (_needsExtendedWindow ? alignedY : shadowPad)
signal dialogClosed
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer {
id: _syncTimer
interval: 0
onTriggered: root._flushSync()
}
property string _chromeClaimId: ""
property bool _fullSyncPending: false
function _nextChromeClaimId() {
return "dms:launcher-v2:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
}
function _currentScreenName() {
return effectiveScreen ? effectiveScreen.name : "";
}
function _publishModalChromeState() {
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalState(screenName, {
"visible": spotlightOpen || contentWindow.visible,
"barSide": resolvedConnectedBarSide,
"bodyX": _connectedChromeX,
"bodyY": _connectedChromeY,
"bodyW": _connectedChromeWidth,
"bodyH": _connectedChromeHeight,
"animX": contentContainer ? contentContainer.animX : 0,
"animY": contentContainer ? contentContainer.animY : 0,
"omitStartConnector": false,
"omitEndConnector": false
});
}
function _syncModalChromeState() {
if (!frameOwnsConnectedChrome) {
_releaseModalChrome();
return;
}
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishModalChromeState();
if (_dockBlocksEmergence && (spotlightOpen || contentWindow.visible))
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
else
ConnectedModeState.releaseDockRetract(_chromeClaimId);
}
property bool _animSyncQueued: false
property bool _bodySyncQueued: false
function _queueFullSync() {
_fullSyncPending = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueAnimSync() {
_animSyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueBodySync() {
_bodySyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _flushSync() {
const fullDirty = _fullSyncPending;
const animDirty = _animSyncQueued;
const bodyDirty = _bodySyncQueued;
_fullSyncPending = false;
_animSyncQueued = false;
_bodySyncQueued = false;
if (fullDirty)
_syncModalChromeState();
if (animDirty)
_syncModalAnim();
if (bodyDirty)
_syncModalBody();
}
function _syncModalAnim() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
return;
const screenName = _currentScreenName();
if (!screenName || !contentContainer)
return;
ConnectedModeState.setModalAnim(screenName, contentContainer.animX, contentContainer.animY);
}
function _syncModalBody() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
return;
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalBody(screenName, _connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight);
}
function _releaseModalChrome() {
if (_chromeClaimId) {
ConnectedModeState.releaseDockRetract(_chromeClaimId);
_chromeClaimId = "";
}
const screenName = _currentScreenName();
if (screenName)
ConnectedModeState.clearModalState(screenName);
}
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
onLauncherArcExtenderActiveChanged: _queueFullSync()
onResolvedConnectedBarSideChanged: _queueFullSync()
onSpotlightOpenChanged: _queueFullSync()
onAlignedXChanged: _queueBodySync()
onAlignedYChanged: _queueBodySync()
onAlignedWidthChanged: _queueBodySync()
onAlignedHeightChanged: _queueBodySync()
Component.onDestruction: _releaseModalChrome()
Connections {
target: contentWindow
function onVisibleChanged() {
if (contentWindow.visible)
root._syncModalChromeState();
else
root._releaseModalChrome();
}
}
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
_pendingInitialize = true;
contentVisible = true;
launcherContentLoader.active = true;
if (spotlightContent) {
_initializeAndShow(_pendingQuery, _pendingMode);
_pendingInitialize = false;
}
}
function _initializeAndShow(query, mode) {
if (!spotlightContent)
return;
contentVisible = true;
// 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;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
if (query) {
spotlightContent.controller.setSearchQuery(query);
} else {
spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll();
}
if (spotlightContent.actionPanel) {
spotlightContent.actionPanel.hide();
}
}
function _openCommon(query, mode) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
// Disable animations so the snap is instant
animationsEnabled = false;
// 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(modalHandle);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab)
focusGrab.active = true;
// 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) {
_openCommon(query, "");
}
function hide() {
if (!spotlightOpen)
return;
openedFromOverview = false;
isClosing = true;
// 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(modalHandle);
closeCleanupTimer.start();
}
function toggle() {
spotlightOpen ? hide() : show();
}
function showWithMode(mode) {
_openCommon("", mode);
}
function toggleWithMode(mode) {
if (spotlightOpen) {
hide();
} else {
showWithMode(mode);
}
}
function toggleWithQuery(query) {
if (spotlightOpen) {
hide();
} else {
showWithQuery(query);
}
}
Timer {
id: closeCleanupTimer
interval: Theme.variantCloseInterval(root.launcherAnimationDuration)
repeat: false
onTriggered: {
isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
}
}
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
}
HyprlandFocusGrab {
id: focusGrab
windows: [contentWindow]
active: false
onCleared: {
if (spotlightOpen) {
hide();
}
}
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== modalHandle && spotlightOpen) {
hide();
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (Quickshell.screens.length === 0)
return;
const screen = contentWindow.screen;
const screenName = screen?.name;
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) {
needsReset = false;
break;
}
}
}
if (!needsReset)
return;
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (!newScreen)
return;
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
}
}
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
readonly property real _topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
readonly property real _bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
readonly property real _leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: backgroundWindow._topMargin
bottom: backgroundWindow._bottomMargin
left: backgroundWindow._leftMargin
right: backgroundWindow._rightMargin
}
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
Region {
item: bgContentHole
intersection: Intersection.Subtract
}
}
Item {
id: bgFullScreenMask
anchors.fill: parent
}
Item {
id: bgContentHole
visible: false
x: root._cwMarginLeft + contentContainer.x - backgroundWindow._leftMargin
y: root._cwMarginTop + contentContainer.y - backgroundWindow._topMargin
width: root.alignedWidth
height: root.contentSurfaceHeight
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
}
}
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: root.hide()
}
}
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0
blurHeight: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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: contentInputMask
visible: false
x: contentContainer.x
y: contentContainer.y
width: root.alignedWidth
height: root.contentSurfaceHeight
}
Item {
id: contentContainer
// 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.contentSurfaceHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real _connectedTravelX: Math.max(Theme.effectAnimOffset, root.alignedWidth + Theme.spacingL)
readonly property real _connectedTravelY: root.launcherArcExtenderActive ? root._connectedChromeHeight : Math.max(Theme.effectAnimOffset, root.alignedHeight + Theme.spacingL)
readonly property real collapsedMotionX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -_connectedTravelX;
case "right":
return _connectedTravelX;
}
return 0;
}
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -_connectedTravelY;
case "bottom":
return _connectedTravelY;
}
return 0;
}
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root._motionActive ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
}
readonly property real animX: root._frozenMotionX * (1 - morph.openProgress)
readonly property real animY: root._frozenMotionY * (1 - morph.openProgress)
readonly property real scaleValue: Theme.effectScaleCollapsed + (1.0 - Theme.effectScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect
readonly property real clipOversize: 2000
clip: shouldClip
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
Item {
id: aligner
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.publishedOpacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.backgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
targetRadius: root.cornerRadius
shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
// contentWrapper moves inside static contentContainer — DankPopout pattern
Item {
id: contentWrapper
width: parent.width
height: parent.height
property bool _renderActive: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || launcherMotionVisible
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
visible: _renderActive
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)
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
onRunningChanged: if (!running && contentWrapper.publishedOpacity === 0)
contentWrapper._renderActive = false
}
}
Connections {
target: root
function onLauncherMotionVisibleChanged() {
if (root.launcherMotionVisible)
contentWrapper._renderActive = 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;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
}

View File

@@ -0,0 +1,512 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
property var modalHandle: root
visible: false
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
property bool _pendingInitialize: false
property string _pendingQuery: ""
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : ""
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2
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)
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad, dpr))
readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: alignedHeight + contentY + shadowPad
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: Theme.cornerRadius
readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium;
switch (SettingsData.dankLauncherV2BorderColor) {
case "primary":
return Theme.primary;
case "secondary":
return Theme.secondary;
case "outline":
return Theme.outline;
case "surfaceText":
return Theme.surfaceText;
default:
return Theme.primary;
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
_pendingInitialize = true;
contentVisible = true;
launcherContentLoader.active = true;
if (spotlightContent) {
_initializeAndShow(_pendingQuery, _pendingMode);
_pendingInitialize = false;
}
}
function _initializeAndShow(query, mode) {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.searchQuery = targetQuery;
spotlightContent.controller.performSearch();
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll();
}
if (spotlightContent.actionPanel) {
spotlightContent.actionPanel.hide();
}
}
function _finishShow(query, mode) {
spotlightOpen = true;
isClosing = false;
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(modalHandle);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query || "", mode || "");
}
function _openCommon(query, mode) {
closeCleanupTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, mode));
return;
}
_finishShow(query, mode);
}
function show() {
_openCommon("", "");
}
function showWithQuery(query) {
_openCommon(query, "");
}
function showWithMode(mode) {
_openCommon("", mode);
}
function hide() {
if (!spotlightOpen)
return;
openedFromOverview = false;
isClosing = true;
contentVisible = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(modalHandle);
closeCleanupTimer.start();
}
function toggle() {
spotlightOpen ? hide() : show();
}
function toggleWithMode(mode) {
if (spotlightOpen) {
hide();
} else {
showWithMode(mode);
}
}
function toggleWithQuery(query) {
if (spotlightOpen) {
hide();
} else {
showWithQuery(query);
}
}
Timer {
id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50
repeat: false
onTriggered: {
isClosing = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
}
}
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
}
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
active: false
onCleared: {
if (spotlightOpen) {
hide();
}
}
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== modalHandle && spotlightOpen) {
hide();
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (Quickshell.screens.length === 0)
return;
const screenName = launcherWindow.screen?.name;
if (screenName) {
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName)
return;
}
}
if (spotlightOpen)
hide();
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (newScreen)
launcherWindow.screen = newScreen;
}
}
PanelWindow {
id: clickCatcher
screen: launcherWindow.screen
visible: spotlightOpen
color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: outsideClickMask
Region {
item: outsideClickHole
intersection: Intersection.Subtract
}
}
Item {
id: outsideClickMask
visible: false
anchors.fill: parent
}
Rectangle {
id: outsideClickHole
visible: false
color: "transparent"
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: root.hide()
}
}
PanelWindow {
id: launcherWindow
visible: spotlightOpen || isClosing
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
blurWidth: contentVisible ? modalContainer.width * s : 0
blurHeight: contentVisible ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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 {
top: true
left: true
}
WlrLayershell.margins {
left: root.windowX
top: root.windowY
right: 0
bottom: 0
}
implicitWidth: root.windowWidth
implicitHeight: root.windowHeight
mask: Region {
item: launcherInputMask
}
Rectangle {
id: launcherInputMask
visible: false
color: "transparent"
x: modalContainer.x
y: modalContainer.y
width: modalContainer.width
height: modalContainer.height
}
Item {
id: modalContainer
x: root.contentX
y: root.contentY
width: root.alignedWidth
height: root.alignedHeight
visible: _renderActive
property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
onRunningChanged: if (!running && !root.contentVisible)
modalContainer._renderActive = false
}
}
Behavior on scale {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on publishedScale {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Connections {
target: root
function onContentVisibleChanged() {
if (root.contentVisible)
modalContainer._renderActive = true;
}
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
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
}
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;
}
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
}
}
}

View File

@@ -41,7 +41,6 @@ FocusScope {
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editDgpuToggle.checked = existing?.launchOnDgpu || false;
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
@@ -65,8 +64,6 @@ FocusScope {
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
if (editDgpuToggle.checked)
override.launchOnDgpu = true;
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
@@ -89,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: {
@@ -149,18 +146,10 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Down:
if (hasCtrl) {
controller.navigateHistory("down");
} else {
controller.selectNext();
}
controller.selectNext();
return;
case Qt.Key_Up:
if (hasCtrl) {
controller.navigateHistory("up");
} else {
controller.selectPrevious();
}
controller.selectPrevious();
return;
case Qt.Key_PageDown:
controller.selectPageDown(8);
@@ -169,10 +158,6 @@ FocusScope {
controller.selectPageUp(8);
return;
case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight();
return;
@@ -180,25 +165,12 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft();
return;
}
event.accepted = false;
return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J:
if (hasCtrl) {
controller.selectNext();
@@ -213,13 +185,6 @@ FocusScope {
}
event.accepted = false;
return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N:
if (hasCtrl) {
controller.selectNextSection();
@@ -235,19 +200,13 @@ FocusScope {
event.accepted = false;
return;
case Qt.Key_Tab:
if (hasCtrl && actionPanel.hasActions) {
if (actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
}
controller.selectNext();
return;
case Qt.Key_Backtab:
if (hasCtrl && actionPanel.expanded) {
const reverse = true;
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
if (actionPanel.expanded)
actionPanel.hide();
return;
case Qt.Key_Return:
case Qt.Key_Enter:
@@ -311,24 +270,29 @@ FocusScope {
Item {
anchors.fill: parent
visible: !editMode && !(root.parentModal?.isClosing ?? false)
visible: !editMode
Item {
id: footerBar
readonly property bool _connectedBottomEmerge: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom")
readonly property bool _connectedArcAtFooter: _connectedBottomEmerge && !(root.parentModal?.launcherArcExtenderActive ?? false)
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: root.parentModal?.borderWidth ?? 1
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
height: showFooter ? 36 : 0
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingM : (root.parentModal?.borderWidth ?? 1)
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
visible: showFooter
clip: true
Rectangle {
anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius
// In connected mode the launcher provides the surface so update the toolbar for arcs
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false)
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
@@ -336,7 +300,7 @@ FocusScope {
Row {
id: modeButtonsRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingXS
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
spacing: 2
@@ -408,7 +372,7 @@ FocusScope {
Row {
id: hintsRow
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
spacing: Theme.spacingM
@@ -429,7 +393,7 @@ FocusScope {
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Ctrl-Tab " + I18n.tr("actions")
text: "Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
visible: actionPanel.hasActions
@@ -503,7 +467,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
@@ -737,6 +701,14 @@ 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: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList {
id: resultsList
anchors.fill: parent
@@ -769,7 +741,6 @@ FocusScope {
}
function onSearchQueryRequested(query) {
searchField.text = query;
searchField.cursorPosition = query.length;
}
function onModeChanged() {
extFilterField.text = "";
@@ -980,15 +951,6 @@ FocusScope {
keyNavigationBacktab: editEnvVarsField
}
}
DankToggle {
id: editDgpuToggle
width: parent.width
text: I18n.tr("Launch on dGPU by default")
visible: SessionService.nvidiaCommand.length > 0
checked: false
onToggled: checked => editDgpuToggle.checked = checked
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtCore
import Quickshell.Io
import qs.Common
import qs.Widgets
@@ -22,9 +21,9 @@ Rectangle {
onShowFileInfoChanged: {
if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
const fullPath = currentPath + "/" + currentFileName;
fileStatProcess.selectedFilePath = fullPath;
fileStatProcess.running = true;
}
}
@@ -38,14 +37,14 @@ Rectangle {
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const parts = text.trim().split('|')
const parts = text.trim().split('|');
if (parts.length >= 4) {
fileStatProcess.fileStats = {
"modifiedTime": parts[0],
"permissions": parts[1],
"size": parseInt(parts[2]) || 0,
"fullPath": parts[3]
}
};
}
}
}
@@ -60,31 +59,31 @@ Rectangle {
onCurrentFileNameChanged: {
if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName
const fullPath = currentPath + "/" + currentFileName;
if (fullPath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
fileStatProcess.selectedFilePath = fullPath;
fileStatProcess.running = true;
}
}
}
function updateFileInfo(filePath, fileName, isDirectory) {
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = filePath
currentFileName = fileName || ""
currentFileIsDir = isDirectory || false
fileStatProcess.selectedFilePath = filePath;
currentFileName = fileName || "";
currentFileIsDir = isDirectory || false;
let ext = ""
let ext = "";
if (!isDirectory && fileName) {
const lastDot = fileName.lastIndexOf('.')
const lastDot = fileName.lastIndexOf('.');
if (lastDot > 0) {
ext = fileName.substring(lastDot + 1).toLowerCase()
ext = fileName.substring(lastDot + 1).toLowerCase();
}
}
currentFileExtension = ext
currentFileExtension = ext;
if (showFileInfo) {
fileStatProcess.running = true
fileStatProcess.running = true;
}
}
}
@@ -100,10 +99,10 @@ Rectangle {
"permissions": "",
"extension": "",
"position": "N/A"
}
};
}
const hasValidFile = currentFileName !== ""
const hasValidFile = currentFileName !== "";
return {
"exists": hasValidFile,
"name": hasValidFile ? currentFileName : "Loading...",
@@ -113,7 +112,7 @@ Rectangle {
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
"extension": currentFileExtension,
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
}
};
}
Column {
@@ -209,23 +208,23 @@ Rectangle {
function formatFileSize(bytes) {
if (bytes === 0 || !bytes) {
return "0 B"
return "0 B";
}
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) {
return "Unknown"
return "Unknown";
}
const parts = dateTimeString.split(' ')
const parts = dateTimeString.split(' ');
if (parts.length >= 2) {
return parts[0] + " " + parts[1].split('.')[0]
return parts[0] + " " + parts[1].split('.')[0];
}
return dateTimeString
return dateTimeString;
}
Behavior on opacity {

View File

@@ -61,26 +61,22 @@ Item {
border.color: Theme.primary
opacity: 0
SequentialAnimation on opacity {
OpacityAnimator on opacity {
running: root.isRunning
loops: Animation.Infinite
NumberAnimation {
from: 0.8
to: 0
duration: 1500
easing.type: Easing.OutQuad
}
from: 0.8
to: 0
duration: 1500
easing.type: Easing.OutQuad
}
SequentialAnimation on scale {
ScaleAnimator on scale {
running: root.isRunning
loops: Animation.Infinite
NumberAnimation {
from: 0.5
to: 1.5
duration: 1500
easing.type: Easing.OutQuad
}
from: 0.5
to: 1.5
duration: 1500
easing.type: Easing.OutQuad
}
}
@@ -95,26 +91,22 @@ Item {
border.color: Theme.secondary
opacity: 0
SequentialAnimation on opacity {
OpacityAnimator on opacity {
running: root.isRunning
loops: Animation.Infinite
NumberAnimation {
from: 0.8
to: 0
duration: 1500
easing.type: Easing.OutQuad
}
from: 0.8
to: 0
duration: 1500
easing.type: Easing.OutQuad
}
SequentialAnimation on scale {
ScaleAnimator on scale {
running: root.isRunning
loops: Animation.Infinite
NumberAnimation {
from: 0.3
to: 1.3
duration: 1500
easing.type: Easing.OutQuad
}
from: 0.3
to: 1.3
duration: 1500
easing.type: Easing.OutQuad
}
}

View File

@@ -112,7 +112,9 @@ FocusScope {
focus: active
sourceComponent: Component {
DockTab {}
DockTab {
parentModal: root.parentModal
}
}
onActiveChanged: {
@@ -218,7 +220,9 @@ FocusScope {
visible: active
focus: active
sourceComponent: ThemeColorsTab {}
sourceComponent: ThemeColorsTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
@@ -518,5 +522,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: frameLoader
anchors.fill: parent
active: root.currentIndex === 33
visible: active
focus: active
sourceComponent: FrameTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}

View File

@@ -120,6 +120,12 @@ Rectangle {
"text": I18n.tr("Widgets"),
"icon": "widgets",
"tabIndex": 22
},
{
"id": "frame",
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
}
]
},