From 64dcb0d34e9f2a373a29aaf6a69b23cdd28c298a Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Thu, 30 Oct 2025 21:34:31 -0400 Subject: [PATCH] Panels: beautifull NRectangleCurved shape - conditionnal with a new settings, default is true. --- Assets/Translations/de.json | 4 + Assets/Translations/en.json | 4 + Assets/Translations/es.json | 4 + Assets/Translations/fr.json | 4 + Assets/Translations/pt.json | 4 + Assets/Translations/zh-CN.json | 4 + Assets/settings-default.json | 1 + Commons/Settings.qml | 1 + Modules/Settings/Tabs/UserInterfaceTab.qml | 7 + Widgets/NPanel.qml | 161 ++++++++++++++---- Widgets/NRectangleCurved.qml | 187 +++++++++++++++++++++ 11 files changed, 346 insertions(+), 35 deletions(-) create mode 100644 Widgets/NRectangleCurved.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 12e903aa..3a706d10 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -872,6 +872,10 @@ "tooltips": { "description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.", "label": "Tooltips anzeigen" + }, + "panels-attached-to-bar": { + "description": "Wenn aktiviert, werden die Panels mit einem schönen, umgekehrten Eckdesign an der Leiste befestigt.", + "label": "Paneele an Stange befestigen" } }, "lock-screen": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index f439976b..50a4a211 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -869,6 +869,10 @@ "label": "Disable UI Animations", "description": "Disable all animations for a faster, more responsive experience." }, + "panels-attached-to-bar": { + "label": "Attach panels to bar", + "description": "When enabled, panels will be attached to the bar with beautiful inverted corner design" + }, "panels-overlay": { "label": "Open panels in overlay layer", "description": "Panels will appear above fullscreen windows" diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 9831032a..381d458a 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -872,6 +872,10 @@ "tooltips": { "description": "Activar o desactivar los avisos emergentes en toda la interfaz.", "label": "Mostrar sugerencias" + }, + "panels-attached-to-bar": { + "description": "Cuando está habilitado, los paneles se adjuntarán a la barra con un hermoso diseño de esquina invertida.", + "label": "Adjuntar paneles a la barra" } }, "lock-screen": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index ac3c1a63..82b990c0 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -872,6 +872,10 @@ "tooltips": { "description": "Activer ou désactiver les info-bulles dans toute l'interface.", "label": "Afficher les infobulles" + }, + "panels-attached-to-bar": { + "description": "Lorsque cette option est activée, les panneaux seront attachés à la barre avec un design élégant de coin inversé.", + "label": "Fixer les panneaux à la barre." } }, "lock-screen": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 09937559..80917f1d 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -872,6 +872,10 @@ "tooltips": { "description": "Ativar ou desativar dicas de ferramentas em toda a interface.", "label": "Mostrar dicas de ferramenta" + }, + "panels-attached-to-bar": { + "description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.", + "label": "Anexar painéis à barra" } }, "lock-screen": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index f9ecddbe..59a36cd3 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -872,6 +872,10 @@ "tooltips": { "description": "启用或禁用整个界面的工具提示。", "label": "显示工具提示" + }, + "panels-attached-to-bar": { + "description": "启用后,面板将以美观的倒角设计附加到栏上。", + "label": "将面板连接到杆上" } }, "lock-screen": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 305befd0..c336803b 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -217,6 +217,7 @@ "fontDefaultScale": 1, "fontFixedScale": 1, "tooltipsEnabled": true, + "panelsAttachedToBar": true, "panelsOverlayLayer": true }, "brightness": { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index baa0727e..f9db3f9a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -354,6 +354,7 @@ Singleton { property real fontDefaultScale: 1.0 property real fontFixedScale: 1.0 property bool tooltipsEnabled: true + property bool panelsAttachedToBar: true property bool panelsOverlayLayer: true } diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index e3190927..690a7f3d 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -33,6 +33,13 @@ ColumnLayout { onToggled: checked => Settings.data.ui.tooltipsEnabled = checked } + NToggle { + label: I18n.tr("settings.user-interface.panels-attached-to-bar.label") + description: I18n.tr("settings.user-interface.panels-attached-to-bar.description") + checked: Settings.data.ui.panelsAttachedToBar + onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked + } + NToggle { label: I18n.tr("settings.user-interface.panels-overlay.label") description: I18n.tr("settings.user-interface.panels-overlay.description") diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 00327550..32bb4fc1 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -9,6 +9,7 @@ Loader { property ShellScreen screen + property bool attachedToBar: Settings.data.ui.panelsAttachedToBar property bool useOverlay: Settings.data.ui.panelsOverlayLayer property Component panelContent: null @@ -39,10 +40,20 @@ Loader { property bool backgroundClickEnabled: true // Animation properties - readonly property real originalScale: 0.0 - property real scaleValue: originalScale + property real panelBackgroundOpacity: 0 + property real panelContentOpacity: 0 property real dimmingOpacity: 0 + readonly property string barPosition: Settings.data.bar.position + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property real verticalBarWidth: Style.barHeight + + // Effective anchor properties - combines explicit anchors with implicit anchoring from useButtonPosition + readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") + readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") + readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") + readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") + signal opened signal closed @@ -97,7 +108,8 @@ Loader { // ----------------------------------------- function close() { dimmingOpacity = 0 - scaleValue = originalScale + panelBackgroundOpacity = 0 + panelContentOpacity = 0 root.closed() active = false useButtonPosition = false @@ -132,10 +144,7 @@ Loader { PanelWindow { id: panelWindow - readonly property string barPosition: Settings.data.bar.position - readonly property bool isVertical: barPosition === "left" || barPosition === "right" readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) - readonly property real verticalBarWidth: Style.barHeight Component.onCompleted: { Logger.d("NPanel", "Opened", root.objectName, "on", screen.name) @@ -195,12 +204,73 @@ Loader { } // The actual panel's content - Rectangle { + NRectangleCurved { id: panelBackground - color: panelBackgroundColor - radius: Style.radiusL - border.color: panelBorderColor - border.width: Style.borderS + + backgroundColor: panelBackgroundColor + + topLeftRadius: Style.radiusL + topRightRadius: Style.radiusL + bottomLeftRadius: Style.radiusL + bottomRightRadius: Style.radiusL + + // Set inverted corners based on panel anchors and bar position + + // Top-left corner + topLeftInverted: { + if (!attachedToBar) return false + + // Inverted if panel is anchored to top edge (bar is at top) + if (effectivePanelAnchorTop) + return true + // Or if panel is anchored to left edge (bar is at left) + if (effectivePanelAnchorLeft) + return true + return false + } + topLeftInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" + + // Top-right corner + topRightInverted: { + if (!attachedToBar) return false + + // Inverted if panel is anchored to top edge (bar is at top) + if (effectivePanelAnchorTop) + return true + // Or if panel is anchored to right edge (bar is at right) + if (effectivePanelAnchorRight) + return true + return false + } + topRightInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" + + // Bottom-left corner + bottomLeftInverted: { + if (!attachedToBar) return false + + // Inverted if panel is anchored to bottom edge (bar is at bottom) + if (effectivePanelAnchorBottom) + return true + // Or if panel is anchored to left edge (bar is at left) + if (effectivePanelAnchorLeft) + return true + return false + } + bottomLeftInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" + + // Bottom-right corner + bottomRightInverted: { + if (!attachedToBar) return false + + // Inverted if panel is anchored to bottom edge (bar is at bottom) + if (effectivePanelAnchorBottom) + return true + // Or if panel is anchored to right edge (bar is at right) + if (effectivePanelAnchorRight) + return true + return false + } + bottomRightInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" // Dragging support property bool draggable: root.draggable @@ -229,7 +299,7 @@ Loader { return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) } - scale: root.scaleValue + opacity: root.panelBackgroundOpacity x: isDragged ? manualX : calculatedX y: isDragged ? manualY : calculatedY @@ -240,11 +310,12 @@ Loader { if (!barIsVisible) { return 0 } + switch (barPosition) { case "top": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) default: - return Style.marginS + return attachedToBar ? 0 : Style.marginS } } @@ -254,9 +325,9 @@ Loader { } switch (barPosition) { case "bottom": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) + return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0) default: - return Style.marginS + return attachedToBar ? 0 : Style.marginS } } @@ -266,9 +337,9 @@ Loader { } switch (barPosition) { case "left": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) default: - return Style.marginS + return attachedToBar ? 0 : Style.marginS } } @@ -278,9 +349,9 @@ Loader { } switch (barPosition) { case "right": - return (Style.barHeight + Style.marginS) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) + return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0) default: - return Style.marginS + return attachedToBar ? 0 : Style.marginS } } @@ -300,7 +371,7 @@ Loader { } // No fixed anchoring - if (isVertical) { + if (barIsVertical) { // Vertical bar if (barPosition === "right") { // To the left of the right bar @@ -341,7 +412,7 @@ Loader { } // No fixed anchoring - if (isVertical) { + if (barIsVertical) { // Vertical bar if (useButtonPosition) { // Position panel relative to button @@ -368,7 +439,28 @@ Loader { // Animate in when component is completed Component.onCompleted: { - root.scaleValue = 1.0 + // Start invisible + // Use a timer to delay the animation start, allowing QML to properly set up initial state + fadeInTimer.start() + } + + Timer { + id: fadeInTimer + interval: 1 + repeat: false + onTriggered: { + // Fade in background + root.panelBackgroundOpacity = 1.0 + } + } + + // Timer to fade in content after slide animation completes + Timer { + id: contentFadeInTimer + interval: Style.animationFast + repeat: false + running: true + onTriggered: root.panelContentOpacity = 1.0 } // Reset drag position when panel closes @@ -384,17 +476,10 @@ Loader { anchors.fill: parent } - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutExpo - } - } - + // Animation behavior Behavior on opacity { NumberAnimation { - duration: Style.animationNormal + duration: Style.animationFast easing.type: Easing.OutQuad } } @@ -403,6 +488,14 @@ Loader { id: panelContentLoader anchors.fill: parent sourceComponent: root.panelContent + opacity: root.panelContentOpacity + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } } // Handle drag move on the whole panel area @@ -460,8 +553,7 @@ Loader { anchors.margins: 0 color: Color.transparent border.color: Color.mPrimary - border.width: Style.borderL - radius: parent.radius + border.width: Style.borderM visible: panelBackground.isDragged && dragHandler.active opacity: 0.8 z: 3000 @@ -473,7 +565,6 @@ Loader { color: Color.transparent border.color: Color.mPrimary border.width: Style.borderS - radius: parent.radius opacity: 0.3 } } diff --git a/Widgets/NRectangleCurved.qml b/Widgets/NRectangleCurved.qml new file mode 100644 index 00000000..a97bb245 --- /dev/null +++ b/Widgets/NRectangleCurved.qml @@ -0,0 +1,187 @@ +import QtQuick +import qs.Commons + +Item { + id: root + + // Corner radius properties + property real topLeftRadius: 0 + property real topRightRadius: 0 + property real bottomLeftRadius: 0 + property real bottomRightRadius: 0 + + // Inverted corner properties (concave instead of convex) + property bool topLeftInverted: false + property bool topRightInverted: false + property bool bottomLeftInverted: false + property bool bottomRightInverted: false + + // Direction for inverted corners: "horizontal" or "vertical" + // horizontal: curves left/right (extends beyond left/right edges) + // vertical: curves up/down (extends beyond top/bottom edges) + property string topLeftInvertedDirection: "horizontal" // default: curves left + property string topRightInvertedDirection: "horizontal" // default: curves right + property string bottomLeftInvertedDirection: "horizontal" // default: curves left + property string bottomRightInvertedDirection: "horizontal" // default: curves right + + // Background color + property color backgroundColor: "white" + + // Check if any corner is inverted + readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted + + // Calculate padding needed for inverted corners based on their direction + readonly property real topPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "vertical") ? topLeftRadius : 0, (topRightInverted && topRightInvertedDirection === "vertical") ? topRightRadius : 0) + readonly property real bottomPadding: Math.max((bottomLeftInverted && bottomLeftInvertedDirection === "vertical") ? bottomLeftRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "vertical") ? bottomRightRadius : 0) + readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0) + readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0) + + // Simple rectangle for non-inverted corners (better performance) + Rectangle { + id: simpleBackground + anchors.fill: parent + color: root.backgroundColor + radius: topLeftRadius // Use topLeftRadius as default + visible: !root.hasInvertedCorners + + topLeftRadius: root.topLeftRadius + topRightRadius: root.topRightRadius + bottomLeftRadius: root.bottomLeftRadius + bottomRightRadius: root.bottomRightRadius + } + + // Background with custom corners (for inverted corners) + Canvas { + id: background + anchors.fill: parent + anchors.topMargin: -root.topPadding + anchors.bottomMargin: -root.bottomPadding + anchors.leftMargin: -root.leftPadding + anchors.rightMargin: -root.rightPadding + visible: root.hasInvertedCorners + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + // Adjust coordinates to account for inverted corner padding + var x = root.leftPadding + var y = root.topPadding + var w = width - root.leftPadding - root.rightPadding + var h = height - root.topPadding - root.bottomPadding + + ctx.fillStyle = root.backgroundColor + ctx.beginPath() + + // Start from top left + if (topLeftInverted) { + if (topLeftInvertedDirection === "vertical") { + ctx.moveTo(x, y) + } else { + ctx.moveTo(x + topLeftRadius, y) + } + } else { + ctx.moveTo(x + topLeftRadius, y) + } + + // Top edge and top right corner + if (topRightInverted) { + if (topRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y) + ctx.lineTo(x + w + topRightRadius, y) + ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) + } else { + // Curves upward + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y - topRightRadius) + ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + topRightRadius) + } + } else { + ctx.lineTo(x + w - topRightRadius, y) + ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius) + } + + // Right edge and bottom right corner + if (bottomRightInverted) { + if (bottomRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y + h - bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - bottomRightRadius, y + h) + } else { + // Curves downward + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w, y + h + bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h) + } + } else { + ctx.lineTo(x + w, y + h - bottomRightRadius) + ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius) + } + + // Bottom edge and bottom left corner + if (bottomLeftInverted) { + if (bottomLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x + bottomLeftRadius, y + h) + ctx.lineTo(x - bottomLeftRadius, y + h) + ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) + } else { + // Curves downward + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h + bottomLeftRadius) + ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h - bottomLeftRadius) + } + } else { + ctx.lineTo(x + bottomLeftRadius, y + h) + ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius) + } + + // Left edge and back to top left corner + if (topLeftInverted) { + if (topLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x, y + topLeftRadius) + ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) + ctx.lineTo(x, y) + ctx.lineTo(x + topLeftRadius, y) + } else { + // Curves upward + ctx.lineTo(x, y + topLeftRadius) + ctx.lineTo(x, y) + ctx.lineTo(x, y - topLeftRadius) + ctx.quadraticCurveTo(x, y, x + topLeftRadius, y) + } + } else { + ctx.lineTo(x, y + topLeftRadius) + ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius) + } + + ctx.closePath() + ctx.fill() + } + } + + // Trigger repaint when properties change + onTopLeftRadiusChanged: background.requestPaint() + onTopRightRadiusChanged: background.requestPaint() + onBottomLeftRadiusChanged: background.requestPaint() + onBottomRightRadiusChanged: background.requestPaint() + onTopLeftInvertedChanged: background.requestPaint() + onTopRightInvertedChanged: background.requestPaint() + onBottomLeftInvertedChanged: background.requestPaint() + onBottomRightInvertedChanged: background.requestPaint() + onTopLeftInvertedDirectionChanged: background.requestPaint() + onTopRightInvertedDirectionChanged: background.requestPaint() + onBottomLeftInvertedDirectionChanged: background.requestPaint() + onBottomRightInvertedDirectionChanged: background.requestPaint() + onBackgroundColorChanged: background.requestPaint() + onWidthChanged: background.requestPaint() + onHeightChanged: background.requestPaint() +}