1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

modals: apply same pattern of multi-window

- fixes excessive repaints
fixes #716
This commit is contained in:
bbedward
2025-11-23 12:07:45 -05:00
parent 62845b470c
commit 42de6fd074
15 changed files with 1282 additions and 1279 deletions

View File

@@ -19,141 +19,141 @@ DankModal {
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
confirmTitle = title || ""
confirmMessage = message || ""
confirmButtonText = "Confirm"
cancelButtonText = "Cancel"
confirmButtonColor = Theme.primary
onConfirm = onConfirmCallback || (() => {})
onCancel = onCancelCallback || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
confirmTitle = title || "";
confirmMessage = message || "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || (() => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
confirmTitle = options.title || ""
confirmMessage = options.message || ""
confirmButtonText = options.confirmText || "Confirm"
cancelButtonText = options.cancelText || "Cancel"
confirmButtonColor = options.confirmColor || Theme.primary
onConfirm = options.onConfirm || (() => {})
onCancel = options.onCancel || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
confirmTitle = options.title || "";
confirmMessage = options.message || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || (() => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function selectButton() {
close()
close();
if (selectedButton === 0) {
if (onCancel) {
onCancel()
onCancel();
}
} else {
if (onConfirm) {
onConfirm()
onConfirm();
}
}
}
shouldBeVisible: false
allowStacking: true
width: 350
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: {
close()
close();
if (onCancel) {
onCancel()
onCancel();
}
}
onOpened: {
Qt.callLater(function () {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
})
modalFocusScope.forceActiveFocus();
modalFocusScope.focus = true;
shouldHaveFocus = true;
});
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {
case Qt.Key_Escape:
close()
close();
if (onCancel) {
onCancel()
onCancel();
}
event.accepted = true
break
event.accepted = true;
break;
case Qt.Key_Left:
case Qt.Key_Up:
keyboardNavigation = true
selectedButton = 0
event.accepted = true
break
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
break;
case Qt.Key_Right:
case Qt.Key_Down:
keyboardNavigation = true
selectedButton = 1
event.accepted = true
break
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = (selectedButton + 1) % 2
event.accepted = true
keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2;
event.accepted = true;
}
break
break;
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2
event.accepted = true
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true;
}
break
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break
break;
case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break
break;
case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break
break;
case Qt.Key_Tab:
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
event.accepted = true
break
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedButton !== -1) {
selectButton()
selectButton();
} else {
selectedButton = 1
selectButton()
selectedButton = 1;
selectButton();
}
event.accepted = true
break
event.accepted = true;
break;
}
}
@@ -210,11 +210,11 @@ DankModal {
radius: Theme.cornerRadius
color: {
if (keyboardNavigation && selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha
return Theme.surfaceVariantAlpha;
}
}
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
@@ -235,8 +235,8 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 0
selectButton()
selectedButton = 0;
selectButton();
}
}
}
@@ -246,13 +246,13 @@ DankModal {
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = confirmButtonColor
const baseColor = confirmButtonColor;
if (keyboardNavigation && selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor
return baseColor;
}
}
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
@@ -273,8 +273,8 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 1
selectButton()
selectedButton = 1;
selectButton();
}
}
}

View File

@@ -1,25 +1,23 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
Item {
id: root
property string layerNamespace: "dms:modal"
WlrLayershell.namespace: layerNamespace
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: CompositorService.getScreenScale(screen)
property real modalWidth: 400
property real modalHeight: 300
property var targetScreen: Quickshell.screens[0]
readonly property real screenWidth: targetScreen ? targetScreen.width : 1920
readonly property real screenHeight: targetScreen ? targetScreen.height : 1080
readonly property real dpr: targetScreen ? CompositorService.getScreenScale(targetScreen) : 1
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
@@ -44,290 +42,342 @@ PanelWindow {
property bool allowStacking: false
property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
signal opened
signal dialogClosed
signal backgroundClicked
readonly property bool useBackgroundWindow: {
const layerOverride = Quickshell.env("DMS_MODAL_LAYER");
return !layerOverride || layerOverride === "overlay";
}
function open() {
ModalManager.openModal(root)
closeTimer.stop()
shouldBeVisible = true
visible = true
shouldHaveFocus = false
ModalManager.openModal(root);
closeTimer.stop();
shouldBeVisible = true;
if (useBackgroundWindow)
backgroundWindow.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
})
shouldHaveFocus = Qt.binding(() => shouldBeVisible);
});
}
function close() {
shouldBeVisible = false
shouldHaveFocus = false
closeTimer.restart()
shouldBeVisible = false;
shouldHaveFocus = false;
closeTimer.restart();
}
function toggle() {
if (shouldBeVisible) {
close()
} else {
open()
}
}
visible: shouldBeVisible
color: "transparent"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
return WlrLayershell.Bottom
case "overlay":
return WlrLayershell.Overlay
case "background":
return WlrLayershell.Background
default:
return WlrLayershell.Top
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!shouldHaveFocus) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
shouldBeVisible ? close() : open();
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close()
close();
}
}
target: ModalManager
}
Timer {
id: closeTimer
interval: animationDuration + 120
onTriggered: {
visible = false
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
root.backgroundClicked()
}
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
if (!shouldBeVisible) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
}
}
}
Item {
id: modalContainer
readonly property real shadowBuffer: 5
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
width: Theme.px(root.width, dpr)
height: Theme.px(root.height, dpr)
x: {
if (positioning === "center") {
return Theme.snap((root.screenWidth - width) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.x, 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;
}
return 0
}
y: {
if (positioning === "center") {
return Theme.snap((root.screenHeight - height) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.y, dpr)
})(), 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;
}
return 0
})(), dpr)
PanelWindow {
id: backgroundWindow
screen: root.targetScreen
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
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
if (!outsideContent)
return;
root.backgroundClicked();
}
}
Behavior on animX {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
Behavior on animY {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
PanelWindow {
id: contentWindow
screen: root.targetScreen
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
return WlrLayershell.Bottom;
case "top":
return WlrLayershell.Top;
case "background":
return WlrLayershell.Background;
default:
return WlrLayershell.Overlay;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
Behavior on scaleValue {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
}
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
}
}
Item {
id: contentContainer
id: modalContainer
x: shadowBuffer
y: shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
layer.enabled: true
layer.smooth: false
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
Behavior on opacity {
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 {
NumberAnimation {
duration: animationDuration
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
Behavior on animY {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
Behavior on scaleValue {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
Item {
id: directContentWrapper
id: animatedContent
anchors.fill: parent
visible: root.directContent !== null
focus: true
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
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
Behavior on opacity {
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Connections {
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
}
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());
}
}
}
}
target: root
}
}
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
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus())
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || root.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close()
event.accepted = true
}
}
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;
}
}
}
}
}