From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../quickshell/Modals/Common/ConfirmModal.qml | 291 ++++++++++++ .../quickshell/Modals/Common/DankModal.qml | 496 +++++++++++++++++++++ .../quickshell/Modals/Common/InputModal.qml | 312 +++++++++++++ 3 files changed, 1099 insertions(+) create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common') diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml new file mode 100644 index 0000000..b9c9b90 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml @@ -0,0 +1,291 @@ +import QtQuick +import qs.Common +import qs.Modals.Common +import qs.Widgets + +DankModal { + id: root + + layerNamespace: "dms:confirm-modal" + keepPopoutsOpen: true + + property string confirmTitle: "" + property string confirmMessage: "" + property string confirmButtonText: "Confirm" + property string cancelButtonText: "Cancel" + property color confirmButtonColor: Theme.primary + property var onConfirm: function () {} + property var onCancel: function () {} + property int selectedButton: -1 + 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(); + } + + 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(); + } + + function selectButton() { + close(); + if (selectedButton === 0) { + if (onCancel) { + onCancel(); + } + } else { + if (onConfirm) { + onConfirm(); + } + } + } + + shouldBeVisible: false + allowStacking: true + modalWidth: 350 + modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160 + enableShadow: true + shouldHaveFocus: true + onBackgroundClicked: { + close(); + if (onCancel) { + onCancel(); + } + } + onOpened: { + Qt.callLater(function () { + modalFocusScope.forceActiveFocus(); + modalFocusScope.focus = true; + shouldHaveFocus = true; + }); + } + modalFocusScope.Keys.onPressed: function (event) { + switch (event.key) { + case Qt.Key_Escape: + close(); + if (onCancel) { + onCancel(); + } + event.accepted = true; + break; + case Qt.Key_Left: + case Qt.Key_Up: + keyboardNavigation = true; + selectedButton = 0; + event.accepted = true; + break; + case Qt.Key_Right: + case Qt.Key_Down: + 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; + } + break; + case Qt.Key_P: + if (event.modifiers & Qt.ControlModifier) { + keyboardNavigation = true; + selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2; + event.accepted = true; + } + break; + case Qt.Key_J: + if (event.modifiers & Qt.ControlModifier) { + keyboardNavigation = true; + selectedButton = 1; + event.accepted = true; + } + break; + case Qt.Key_K: + if (event.modifiers & Qt.ControlModifier) { + keyboardNavigation = true; + selectedButton = 0; + event.accepted = true; + } + break; + case Qt.Key_H: + if (event.modifiers & Qt.ControlModifier) { + keyboardNavigation = true; + selectedButton = 0; + event.accepted = true; + } + break; + case Qt.Key_L: + if (event.modifiers & Qt.ControlModifier) { + keyboardNavigation = true; + selectedButton = 1; + event.accepted = true; + } + break; + case Qt.Key_Tab: + 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(); + } else { + selectedButton = 1; + selectButton(); + } + event.accepted = true; + break; + } + } + + content: Component { + Item { + anchors.fill: parent + implicitHeight: mainColumn.implicitHeight + + Column { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + anchors.topMargin: Theme.spacingL + spacing: 0 + + StyledText { + text: confirmTitle + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + + Item { + width: 1 + height: Theme.spacingL + } + + StyledText { + text: confirmMessage + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Item { + width: 1 + height: Theme.spacingL * 1.5 + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + if (keyboardNavigation && selectedButton === 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } else if (cancelButton.containsMouse) { + return Theme.surfacePressed; + } else { + return Theme.surfaceVariantAlpha; + } + } + border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent" + border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0 + + StyledText { + text: cancelButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: cancelButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + selectedButton = 0; + selectButton(); + } + } + } + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + const baseColor = confirmButtonColor; + if (keyboardNavigation && selectedButton === 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); + } else { + return baseColor; + } + } + border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent" + border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0 + + StyledText { + text: confirmButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.primaryText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: confirmButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + selectedButton = 1; + selectButton(); + } + } + } + } + + Item { + width: 1 + height: Theme.spacingL + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml new file mode 100644 index 0000000..cafa133 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml @@ -0,0 +1,496 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string layerNamespace: "dms:modal" + property alias content: contentLoader.sourceComponent + property alias contentLoader: contentLoader + property Item directContent: null + property real modalWidth: 400 + property real modalHeight: 300 + property var targetScreen + readonly property var effectiveScreen: contentWindow.screen ?? targetScreen + readonly property real screenWidth: effectiveScreen?.width ?? 1920 + readonly property real screenHeight: effectiveScreen?.height ?? 1080 + readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 + property bool showBackground: true + property real backgroundOpacity: 0.5 + property string positioning: "center" + property point customPosition: Qt.point(0, 0) + property bool closeOnEscapeKey: true + property bool closeOnBackgroundClick: true + property string animationType: "scale" + property int animationDuration: Theme.modalAnimationDuration + property real animationScaleCollapsed: 0.96 + property real animationOffset: Theme.spacingL + property list animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial + property list 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 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(); + 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)); + } + + function close() { + shouldBeVisible = false; + shouldHaveFocus = false; + ModalManager.closeModal(root); + closeTimer.restart(); + } + + function instantClose() { + animationsEnabled = false; + shouldBeVisible = false; + shouldHaveFocus = false; + ModalManager.closeModal(root); + closeTimer.stop(); + contentWindow.visible = false; + if (!useSingleWindow) + clickCatcher.visible = false; + dialogClosed(); + Qt.callLater(() => animationsEnabled = true); + } + + function toggle() { + shouldBeVisible ? close() : open(); + } + + Connections { + target: ModalManager + function onCloseAllModalsExcept(excludedModal) { + if (excludedModal !== root && !allowStacking && shouldBeVisible) + close(); + } + } + + Connections { + target: Quickshell + function onScreensChanged() { + if (!contentWindow.screen) + return; + const currentScreenName = contentWindow.screen.name; + let screenStillExists = false; + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name === currentScreenName) { + screenStillExists = true; + break; + } + } + if (screenStillExists) + return; + const newScreen = CompositorService.getFocusedScreen(); + if (newScreen) { + contentWindow.screen = newScreen; + if (!useSingleWindow) + clickCatcher.screen = newScreen; + } + } + } + + Timer { + id: closeTimer + interval: 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 + } + + 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 && animatedContent.opacity > 0) ? modalContainer.width * s : 0 + blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? 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": + console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "background": + console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); + return WlrLayershell.Top; + case "overlay": + return WlrLayershell.Overlay; + default: + return WlrLayershell.Top; + } + } + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: { + if (customKeyboardFocus !== null) + return customKeyboardFocus; + if (!shouldHaveFocus) + return WlrKeyboardFocus.None; + if (root.useHyprlandFocusGrab) + return WlrKeyboardFocus.OnDemand; + return WlrKeyboardFocus.Exclusive; + } + + anchors { + left: true + top: true + right: root.useSingleWindow + bottom: root.useSingleWindow + } + + 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" + } + + 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; + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml new file mode 100644 index 0000000..c65f3e0 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml @@ -0,0 +1,312 @@ +import QtQuick +import qs.Common +import qs.Modals.Common +import qs.Widgets + +DankModal { + id: root + + layerNamespace: "dms:input-modal" + keepPopoutsOpen: true + + property string inputTitle: "" + property string inputMessage: "" + property string inputPlaceholder: "" + property string inputText: "" + property string confirmButtonText: "Confirm" + property string cancelButtonText: "Cancel" + property color confirmButtonColor: Theme.primary + property var onConfirm: function (text) {} + property var onCancel: function () {} + property int selectedButton: -1 + property bool keyboardNavigation: false + + function show(title, message, onConfirmCallback, onCancelCallback) { + inputTitle = title || ""; + inputMessage = message || ""; + inputPlaceholder = ""; + inputText = ""; + confirmButtonText = "Confirm"; + cancelButtonText = "Cancel"; + confirmButtonColor = Theme.primary; + onConfirm = onConfirmCallback || ((text) => {}); + onCancel = onCancelCallback || (() => {}); + selectedButton = -1; + keyboardNavigation = false; + open(); + } + + function showWithOptions(options) { + inputTitle = options.title || ""; + inputMessage = options.message || ""; + inputPlaceholder = options.placeholder || ""; + inputText = options.initialText || ""; + confirmButtonText = options.confirmText || "Confirm"; + cancelButtonText = options.cancelText || "Cancel"; + confirmButtonColor = options.confirmColor || Theme.primary; + onConfirm = options.onConfirm || ((text) => {}); + onCancel = options.onCancel || (() => {}); + selectedButton = -1; + keyboardNavigation = false; + open(); + } + + function confirmAndClose() { + const text = inputText; + close(); + if (onConfirm) { + onConfirm(text); + } + } + + function cancelAndClose() { + close(); + if (onCancel) { + onCancel(); + } + } + + function selectButton() { + if (selectedButton === 0) { + cancelAndClose(); + } else { + confirmAndClose(); + } + } + + shouldBeVisible: false + allowStacking: true + modalWidth: 350 + modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200 + enableShadow: true + shouldHaveFocus: true + onBackgroundClicked: cancelAndClose() + onOpened: { + Qt.callLater(function () { + if (contentLoader.item && contentLoader.item.textInputRef) { + contentLoader.item.textInputRef.forceActiveFocus(); + } + }); + } + + content: Component { + FocusScope { + anchors.fill: parent + implicitHeight: mainColumn.implicitHeight + focus: true + + property alias textInputRef: textInput + + Keys.onPressed: function (event) { + const textFieldFocused = textInput.activeFocus; + + switch (event.key) { + case Qt.Key_Escape: + root.cancelAndClose(); + event.accepted = true; + break; + case Qt.Key_Tab: + if (textFieldFocused) { + root.keyboardNavigation = true; + root.selectedButton = 0; + textInput.focus = false; + } else { + root.keyboardNavigation = true; + if (root.selectedButton === -1) { + root.selectedButton = 0; + } else if (root.selectedButton === 0) { + root.selectedButton = 1; + } else { + root.selectedButton = -1; + textInput.forceActiveFocus(); + } + } + event.accepted = true; + break; + case Qt.Key_Left: + if (!textFieldFocused) { + root.keyboardNavigation = true; + root.selectedButton = 0; + event.accepted = true; + } + break; + case Qt.Key_Right: + if (!textFieldFocused) { + root.keyboardNavigation = true; + root.selectedButton = 1; + event.accepted = true; + } + break; + case Qt.Key_Return: + case Qt.Key_Enter: + if (root.selectedButton !== -1) { + root.selectButton(); + } else { + root.confirmAndClose(); + } + event.accepted = true; + break; + } + } + + Column { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + anchors.topMargin: Theme.spacingL + spacing: 0 + + StyledText { + text: root.inputTitle + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + + Item { + width: 1 + height: Theme.spacingL + } + + StyledText { + text: root.inputMessage + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + visible: root.inputMessage !== "" + } + + Item { + width: 1 + height: root.inputMessage !== "" ? Theme.spacingL : 0 + visible: root.inputMessage !== "" + } + + Rectangle { + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: Theme.surfaceVariantAlpha + border.color: textInput.activeFocus ? Theme.primary : "transparent" + border.width: textInput.activeFocus ? 1 : 0 + + TextInput { + id: textInput + + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + selectionColor: Theme.primary + selectedTextColor: Theme.primaryText + clip: true + text: root.inputText + onTextChanged: root.inputText = text + + StyledText { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) + text: root.inputPlaceholder + visible: textInput.text === "" && !textInput.activeFocus + } + } + } + + Item { + width: 1 + height: Theme.spacingL * 1.5 + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + if (root.keyboardNavigation && root.selectedButton === 0) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); + } else if (cancelButton.containsMouse) { + return Theme.surfacePressed; + } else { + return Theme.surfaceVariantAlpha; + } + } + border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent" + border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0 + + StyledText { + text: root.cancelButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: cancelButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.cancelAndClose() + } + } + + Rectangle { + width: 120 + height: 40 + radius: Theme.cornerRadius + color: { + const baseColor = root.confirmButtonColor; + if (root.keyboardNavigation && root.selectedButton === 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); + } else { + return baseColor; + } + } + border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent" + border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0 + + StyledText { + text: root.confirmButtonText + font.pixelSize: Theme.fontSizeMedium + color: Theme.primaryText + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + id: confirmButton + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.confirmAndClose() + } + } + } + + Item { + width: 1 + height: Theme.spacingL + } + } + } + } +} -- cgit v1.3