diff --git a/Commons/I18n.qml b/Commons/I18n.qml index 1399700cf..a52414448 100644 --- a/Commons/I18n.qml +++ b/Commons/I18n.qml @@ -279,7 +279,7 @@ Singleton { interpolations = {} if (!isLoaded) { - Logger.d("I18n", "Translations not loaded yet") + //Logger.d("I18n", "Translations not loaded yet") return key } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3215f0ab6..59365b61a 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -14,12 +14,15 @@ import qs.Modules.Bar.Extras Item { id: root - // This property will be set by NFullScreenWindow + // This property will be set by MainScreen property ShellScreen screen: null // Expose bar region for click-through mask readonly property var barRegion: barContentLoader.item?.children[0] || null + // Expose the actual bar Item for unified background system + readonly property var barItem: barRegion + // Bar positioning properties readonly property string barPosition: Settings.data.bar.position || "top" readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" @@ -61,8 +64,8 @@ Item { sourceComponent: Item { anchors.fill: parent - // Background fill - NShapedRectangle { + // Bar container (background rendered by AllBackgrounds) + Item { id: bar // Position and size the bar based on orientation and floating margins @@ -92,26 +95,82 @@ Item { return baseHeight // Vertical bars extend via width, not height } - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + // Corner states for new unified background system + // State -1: No radius (flat/square corner) + // State 0: Normal (inner curve) + // State 1: Horizontal inversion (outer curve on X-axis) + // State 2: Vertical inversion (outer curve on Y-axis) + readonly property int topLeftCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0 + // Top bar: top corners against screen edge = no radius + if (barPosition === "top") + return -1 + // Left bar: top-left against screen edge = no radius + if (barPosition === "left") + return -1 + // Bottom/Right bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) { + return barIsVertical ? 1 : 2 // horizontal invert for vertical bars, vertical invert for horizontal + } + // No outerCorners = square + return -1 + } - // Floating bar rounded corners - topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0 - topRightRadius: Settings.data.bar.floating || topRightInverted ? Style.radiusL : 0 - bottomLeftRadius: Settings.data.bar.floating || bottomLeftInverted ? Style.radiusL : 0 - bottomRightRadius: Settings.data.bar.floating || bottomRightInverted ? Style.radiusL : 0 + readonly property int topRightCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0 + // Top bar: top corners against screen edge = no radius + if (barPosition === "top") + return -1 + // Right bar: top-right against screen edge = no radius + if (barPosition === "right") + return -1 + // Bottom/Left bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) { + return barIsVertical ? 1 : 2 + } + // No outerCorners = square + return -1 + } - topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right") - topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" - topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left") - topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" + readonly property int bottomLeftCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0 + // Bottom bar: bottom corners against screen edge = no radius + if (barPosition === "bottom") + return -1 + // Left bar: bottom-left against screen edge = no radius + if (barPosition === "left") + return -1 + // Top/Right bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) { + return barIsVertical ? 1 : 2 + } + // No outerCorners = square + return -1 + } - bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right") - bottomLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" - bottomRightInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left") - bottomRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" - - // No border on the bar - borderWidth: 0 + readonly property int bottomRightCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0 + // Bottom bar: bottom corners against screen edge = no radius + if (barPosition === "bottom") + return -1 + // Right bar: bottom-right against screen edge = no radius + if (barPosition === "right") + return -1 + // Top/Left bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) { + return barIsVertical ? 1 : 2 + } + // No outerCorners = square + return -1 + } MouseArea { anchors.fill: parent diff --git a/Modules/Bar/Widgets/AudioVisualizer.qml b/Modules/Bar/Widgets/AudioVisualizer.qml index 0eb3438b1..01615d18a 100644 --- a/Modules/Bar/Widgets/AudioVisualizer.qml +++ b/Modules/Bar/Widgets/AudioVisualizer.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell import qs.Commons -import qs.Modules.AudioSpectrum +import qs.Widgets.AudioSpectrum import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index ca3c78799..615ec16ea 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import qs.Commons import qs.Modules.Bar.Extras -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 222b08246..ab8c3982b 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -5,7 +5,7 @@ import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Modules.Bar.Extras Item { diff --git a/Modules/Bar/Widgets/LockKeys.qml b/Modules/Bar/Widgets/LockKeys.qml index cc1d7443b..3fd5b8fba 100644 --- a/Modules/Bar/Widgets/LockKeys.qml +++ b/Modules/Bar/Widgets/LockKeys.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index bbe671e1c..b1b0b6bc8 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import qs.Modules.AudioSpectrum +import qs.Widgets.AudioSpectrum import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index f0a8cb3b8..23946dfb8 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 06580d4e3..5524a5bdf 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -4,7 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index bb4ec296b..706d69e16 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets @@ -353,13 +353,4 @@ Rectangle { } } } - - // MouseArea { - // anchors.fill: parent - // acceptedButtons: Qt.RightButton - // onClicked: { - // var directPanel = PanelService.getPanel("directWidgetSettingsPanel") - // directPanel.openWidgetSettings(root.section, root.sectionWidgetIndex, root.widgetId, root.widgetSettings) - // } - // } } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index a44dc2d0b..5ab6857e7 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -346,28 +346,28 @@ Rectangle { } } - PanelWindow { - id: trayPanel - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - visible: false - color: Color.transparent - screen: screen + // PanelWindow { + // id: trayPanel + // anchors.top: true + // anchors.left: true + // anchors.right: true + // anchors.bottom: true + // visible: false + // color: Color.transparent + // screen: screen - function open() { - visible = true - } + // function open() { + // visible = true + // } - function close() { - visible = false - } + // function close() { + // visible = false + // } - // Clicking outside of the rectangle to close - MouseArea { - anchors.fill: parent - onClicked: trayPanel.close() - } - } + // // Clicking outside of the rectangle to close + // MouseArea { + // anchors.fill: parent + // onClicked: trayPanel.close() + // } + // } } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 0c603c6f1..5a178e371 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 53f357db3..e2c3d142d 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -11,8 +11,7 @@ import Quickshell.Widgets import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.AudioSpectrum -import qs.Modules.Bar.Calendar +import qs.Widgets.AudioSpectrum Loader { id: lockScreen diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml new file mode 100644 index 000000000..403df4e3c --- /dev/null +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -0,0 +1,161 @@ +import QtQuick +import QtQuick.Shapes +import QtQuick.Effects +import qs.Commons + + +/** + * AllBackgrounds - Unified Shape container for all bar and panel backgrounds + * + * Unified shadow system. This component contains a single Shape + * with multiple ShapePath children (one for bar, one for each panel type). + * + * Benefits: + * - Single GPU-accelerated rendering pass for all backgrounds + * - Unified shadow system (one MultiEffect for everything) + */ +Item { + id: root + + // Reference Bar + required property var bar + + // Reference to MainScreen (for panel access) + required property var windowRoot + + anchors.fill: parent + + // The unified Shape container + Shape { + id: backgroundsShape + anchors.fill: parent + + // Use curve renderer for smooth corners + preferredRendererType: Shape.CurveRenderer + + // CRITICAL: Shape must not block mouse input! + // ShapePaths inside will render, but the Shape container itself should be transparent to input + enabled: false // Disable mouse input on the Shape itself + + Component.onCompleted: { + Logger.d("AllBackgrounds", "AllBackgrounds initialized") + Logger.d("AllBackgrounds", " bar:", root.bar) + Logger.d("AllBackgrounds", " windowRoot:", root.windowRoot) + } + + + /** + * Bar + */ + BarBackground { + bar: root.bar + shapeContainer: backgroundsShape + } + + + /** + * Panels + */ + + // Audio + PanelBackground { + panel: root.windowRoot.audioPanel + shapeContainer: backgroundsShape + } + + // Battery + PanelBackground { + panel: root.windowRoot.batteryPanel + shapeContainer: backgroundsShape + } + + // Bluetooth + PanelBackground { + panel: root.windowRoot.bluetoothPanel + shapeContainer: backgroundsShape + } + + // Calendar + PanelBackground { + panel: root.windowRoot.calendarPanel + shapeContainer: backgroundsShape + } + + // Control Center + PanelBackground { + panel: root.windowRoot.controlCenterPanel + shapeContainer: backgroundsShape + } + + // Launcher + PanelBackground { + panel: root.windowRoot.launcherPanel + shapeContainer: backgroundsShape + } + + // Notification History + PanelBackground { + panel: root.windowRoot.notificationHistoryPanel + shapeContainer: backgroundsShape + } + + // Session Menu + PanelBackground { + panel: root.windowRoot.sessionMenuPanel + shapeContainer: backgroundsShape + } + + // Settings + PanelBackground { + panel: root.windowRoot.settingsPanel + shapeContainer: backgroundsShape + } + + // Setup Wizard + PanelBackground { + panel: root.windowRoot.setupWizardPanel + shapeContainer: backgroundsShape + } + + // TrayDropdown + PanelBackground { + panel: root.windowRoot.trayDropdownPanel + shapeContainer: backgroundsShape + } + + // TrayMenu + PanelBackground { + panel: root.windowRoot.trayMenuPanel + shapeContainer: backgroundsShape + } + + // Wallpaper + PanelBackground { + panel: root.windowRoot.wallpaperPanel + shapeContainer: backgroundsShape + } + + // WiFi + PanelBackground { + panel: root.windowRoot.wifiPanel + shapeContainer: backgroundsShape + } + + // // Tray Dropdown + // PanelBackground { + // panel: root.windowRoot.trayDropdownPanel + // shapeContainer: backgroundsShape + // } + } + + // Unified shadow system (one MultiEffect for all backgrounds) + MultiEffect { + source: backgroundsShape + anchors.fill: backgroundsShape + shadowEnabled: true + shadowBlur: Style.shadowBlur + shadowColor: Color.black + shadowHorizontalOffset: Settings.data.general.shadowOffsetX + shadowVerticalOffset: Settings.data.general.shadowOffsetY + } +} diff --git a/Modules/MainScreen/Backgrounds/BarBackground.qml b/Modules/MainScreen/Backgrounds/BarBackground.qml new file mode 100644 index 000000000..7b1bde40e --- /dev/null +++ b/Modules/MainScreen/Backgrounds/BarBackground.qml @@ -0,0 +1,152 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons + + +/** + * BarBackground - ShapePath component for rendering the bar background + * + * Unified shadow system. This component is a ShapePath that will be + * a child of the unified AllBackgrounds Shape container. + * + * Uses 4-state per-corner system for flexible corner rendering: + * - State -1: No radius (flat/square corner) + * - State 0: Normal (inner curve) + * - State 1: Horizontal inversion (outer curve on X-axis) + * - State 2: Vertical inversion (outer curve on Y-axis) + */ +ShapePath { + id: root + + // Required reference to the bar component + required property var bar + + // Required reference to AllBackgrounds shapeContainer + required property var shapeContainer + + // Corner radius (from Style) + readonly property real radius: Style.radiusL + + // Bar position - since bar's parent fills the screen and Shape also fills the screen, + // we can use bar.x and bar.y directly (they're already in screen coordinates) + readonly property point barMappedPos: bar ? Qt.point(bar.x, bar.y) : Qt.point(0, 0) + + // Flatten corners if bar is too small (handle null bar) + readonly property bool shouldFlatten: bar ? (bar.width < radius * 2 || bar.height < radius * 2) : false + readonly property real effectiveRadius: shouldFlatten ? (bar ? Math.min(bar.width / 2, bar.height / 2) : 0) : radius + + // Helper functions (inlined from ShapeCornerHelper) + function getMultX(cornerState) { + return cornerState === 1 ? -1 : 1 + } + function getMultY(cornerState) { + return cornerState === 2 ? -1 : 1 + } + function getArcDirection(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise + } + function getCornerRadius(cornerState) { + // State -1 = no radius (flat corner) + if (cornerState === -1) + return 0 + // All other states use effectiveRadius + return effectiveRadius + } + + // Per-corner multipliers and radii based on bar's corner states (handle null bar) + readonly property real tlMultX: bar ? getMultX(bar.topLeftCornerState) : 1 + readonly property real tlMultY: bar ? getMultY(bar.topLeftCornerState) : 1 + readonly property real tlRadius: bar ? getCornerRadius(bar.topLeftCornerState) : 0 + + readonly property real trMultX: bar ? getMultX(bar.topRightCornerState) : 1 + readonly property real trMultY: bar ? getMultY(bar.topRightCornerState) : 1 + readonly property real trRadius: bar ? getCornerRadius(bar.topRightCornerState) : 0 + + readonly property real brMultX: bar ? getMultX(bar.bottomRightCornerState) : 1 + readonly property real brMultY: bar ? getMultY(bar.bottomRightCornerState) : 1 + readonly property real brRadius: bar ? getCornerRadius(bar.bottomRightCornerState) : 0 + + readonly property real blMultX: bar ? getMultX(bar.bottomLeftCornerState) : 1 + readonly property real blMultY: bar ? getMultY(bar.bottomLeftCornerState) : 1 + readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0 + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + fillColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + + // Starting position (top-left corner, after the arc) + // Use mapped coordinates relative to the Shape container + startX: barMappedPos.x + tlRadius * tlMultX + startY: barMappedPos.y + + // Smooth color animation + Behavior on fillColor { + ColorAnimation { + duration: Style.animationFast + } + } + + // ========== PATH DEFINITION ========== + // Draws a rectangle with potentially inverted corners + // All coordinates are relative to startX/startY + + // Top edge (moving right) + PathLine { + relativeX: (bar ? bar.width : 0) - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX + relativeY: 0 + } + + // Top-right corner arc + PathArc { + relativeX: root.trRadius * root.trMultX + relativeY: root.trRadius * root.trMultY + radiusX: root.trRadius + radiusY: root.trRadius + direction: root.getArcDirection(root.trMultX, root.trMultY) + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: (bar ? bar.height : 0) - root.trRadius * root.trMultY - root.brRadius * root.brMultY + } + + // Bottom-right corner arc + PathArc { + relativeX: -root.brRadius * root.brMultX + relativeY: root.brRadius * root.brMultY + radiusX: root.brRadius + radiusY: root.brRadius + direction: root.getArcDirection(root.brMultX, root.brMultY) + } + + // Bottom edge (moving left) + PathLine { + relativeX: -((bar ? bar.width : 0) - root.brRadius * root.brMultX - root.blRadius * root.blMultX) + relativeY: 0 + } + + // Bottom-left corner arc + PathArc { + relativeX: -root.blRadius * root.blMultX + relativeY: -root.blRadius * root.blMultY + radiusX: root.blRadius + radiusY: root.blRadius + direction: root.getArcDirection(root.blMultX, root.blMultY) + } + + // Left edge (moving up) - closes the path back to start + PathLine { + relativeX: 0 + relativeY: -((bar ? bar.height : 0) - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY) + } + + // Top-left corner arc (back to start) + PathArc { + relativeX: root.tlRadius * root.tlMultX + relativeY: -root.tlRadius * root.tlMultY + radiusX: root.tlRadius + radiusY: root.tlRadius + direction: root.getArcDirection(root.tlMultX, root.tlMultY) + } +} diff --git a/Modules/MainScreen/Backgrounds/PanelBackground.qml b/Modules/MainScreen/Backgrounds/PanelBackground.qml new file mode 100644 index 000000000..be96b6af7 --- /dev/null +++ b/Modules/MainScreen/Backgrounds/PanelBackground.qml @@ -0,0 +1,174 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons + + +/** + * PanelBackground - ShapePath component for rendering a single background + * + * Unified shadow system. This component is a ShapePath that will + * be a child of the unified AllBackgrounds Shape container. + * + * Uses 4-state per-corner system for flexible corner rendering: + * - State -1: No radius (flat/square corner) + * - State 0: Normal (inner curve) + * - State 1: Horizontal inversion (outer curve on X-axis) + * - State 2: Vertical inversion (outer curve on Y-axis) + */ +ShapePath { + id: root + + // Required reference to the panel component (e.g., windowRoot.controlCenterPanel) + required property var panel + + // Required reference to AllBackgrounds shapeContainer + required property var shapeContainer + + // Corner radius (from Style) + readonly property real radius: Style.radiusL + + // Get the actual panelBackground Item from SmartPanel + // Only access panelRegion if panel exists and is visible + readonly property var panelBg: (panel && panel.visible) ? panel.panelRegion : null + + // Panel position - panelBg is in screen coordinates already + readonly property real panelX: panelBg ? panelBg.x : 0 + readonly property real panelY: panelBg ? panelBg.y : 0 + readonly property real panelWidth: panelBg ? panelBg.width : 0 + readonly property real panelHeight: panelBg ? panelBg.height : 0 + + // Flatten corners if panel is too small + readonly property bool shouldFlatten: panelBg ? (panelWidth < radius * 2 || panelHeight < radius * 2) : false + readonly property real effectiveRadius: shouldFlatten ? Math.min(panelWidth / 2, panelHeight / 2) : radius + + // Helper functions (inlined from ShapeCornerHelper) + function getMultX(cornerState) { + return cornerState === 1 ? -1 : 1 + } + function getMultY(cornerState) { + return cornerState === 2 ? -1 : 1 + } + function getArcDirection(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise + } + function getCornerRadius(cornerState) { + // State -1 = no radius (flat corner) + if (cornerState === -1) + return 0 + // All other states use effectiveRadius + return effectiveRadius + } + + // Per-corner multipliers and radii based on panelBg's corner states + readonly property real tlMultX: panelBg ? getMultX(panelBg.topLeftCornerState) : 1 + readonly property real tlMultY: panelBg ? getMultY(panelBg.topLeftCornerState) : 1 + readonly property real tlRadius: panelBg ? getCornerRadius(panelBg.topLeftCornerState) : 0 + + readonly property real trMultX: panelBg ? getMultX(panelBg.topRightCornerState) : 1 + readonly property real trMultY: panelBg ? getMultY(panelBg.topRightCornerState) : 1 + readonly property real trRadius: panelBg ? getCornerRadius(panelBg.topRightCornerState) : 0 + + readonly property real brMultX: panelBg ? getMultX(panelBg.bottomRightCornerState) : 1 + readonly property real brMultY: panelBg ? getMultY(panelBg.bottomRightCornerState) : 1 + readonly property real brRadius: panelBg ? getCornerRadius(panelBg.bottomRightCornerState) : 0 + + readonly property real blMultX: panelBg ? getMultX(panelBg.bottomLeftCornerState) : 1 + readonly property real blMultY: panelBg ? getMultY(panelBg.bottomLeftCornerState) : 1 + readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0 + + // DEBUG: Log panel state changes + onPanelChanged: { + Logger.d("PanelBackground", "=== panel changed:", panel) + Logger.d("PanelBackground", " panel.visible:", panel ? panel.visible : "null") + Logger.d("PanelBackground", " panel.panelRegion:", panel ? panel.panelRegion : "null") + } + + onPanelBgChanged: { + Logger.d("PanelBackground", "=== panelBg changed:", panelBg) + if (panelBg) { + Logger.d("PanelBackground", " Geometry:", panelX, panelY, panelWidth, "x", panelHeight) + Logger.d("PanelBackground", " startX:", startX, "startY:", startY) + Logger.d("PanelBackground", " fillColor:", fillColor) + } + } + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + + // Starting position (top-left corner, after the arc) + startX: panelX + tlRadius * tlMultX + startY: panelY + + fillColor: Color.mSurface + + // Smooth color animation + Behavior on fillColor { + ColorAnimation { + duration: Style.animationFast + } + } + + // ========== PATH DEFINITION ========== + // Draws a rectangle with potentially inverted corners + // All coordinates are relative to startX/startY + + // Top edge (moving right) + PathLine { + relativeX: root.panelWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX + relativeY: 0 + } + + // Top-right corner arc + PathArc { + relativeX: root.trRadius * root.trMultX + relativeY: root.trRadius * root.trMultY + radiusX: root.trRadius + radiusY: root.trRadius + direction: root.getArcDirection(root.trMultX, root.trMultY) + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: root.panelHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY + } + + // Bottom-right corner arc + PathArc { + relativeX: -root.brRadius * root.brMultX + relativeY: root.brRadius * root.brMultY + radiusX: root.brRadius + radiusY: root.brRadius + direction: root.getArcDirection(root.brMultX, root.brMultY) + } + + // Bottom edge (moving left) + PathLine { + relativeX: -(root.panelWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX) + relativeY: 0 + } + + // Bottom-left corner arc + PathArc { + relativeX: -root.blRadius * root.blMultX + relativeY: -root.blRadius * root.blMultY + radiusX: root.blRadius + radiusY: root.blRadius + direction: root.getArcDirection(root.blMultX, root.blMultY) + } + + // Left edge (moving up) - closes the path back to start + PathLine { + relativeX: 0 + relativeY: -(root.panelHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY) + } + + // Top-left corner arc (back to start) + PathArc { + relativeX: root.tlRadius * root.tlMultX + relativeY: -root.tlRadius * root.tlMultY + radiusX: root.tlRadius + radiusY: root.tlRadius + direction: root.getArcDirection(root.tlMultX, root.tlMultY) + } +} diff --git a/Modules/MainScreen/BarExclusionZone.qml b/Modules/MainScreen/BarExclusionZone.qml new file mode 100644 index 000000000..d80723553 --- /dev/null +++ b/Modules/MainScreen/BarExclusionZone.qml @@ -0,0 +1,74 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons + + +/** + * BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar + * + * This is a minimal window that works with the compositor to reserve space, + * while the actual bar UI is rendered in MainScreen. + */ +PanelWindow { + id: root + + property bool exclusive: Settings.data.bar.exclusive !== undefined ? Settings.data.bar.exclusive : false + + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + + // Invisible - just reserves space + color: "transparent" + + mask: Region {} + + // Wayland layer shell configuration + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore + + // Anchor based on bar position + anchors { + top: barPosition === "top" + bottom: barPosition === "bottom" + left: barPosition === "left" || barPosition === "top" || barPosition === "bottom" + right: barPosition === "right" || barPosition === "top" || barPosition === "bottom" + } + + // Size based on bar orientation + // When floating, only reserve space for the bar + margin on the anchored edge + implicitWidth: { + if (barIsVertical) { + // Vertical bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For left bar, reserve left margin; for right bar, reserve right margin + return Style.barHeight + barMarginH + } + return Style.barHeight + } + return 0 // Auto-width when left/right anchors are true + } + + implicitHeight: { + if (!barIsVertical) { + // Horizontal bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For top bar, reserve top margin; for bottom bar, reserve bottom margin + return Style.barHeight + barMarginV + } + return Style.barHeight + } + return 0 // Auto-height when top/bottom anchors are true + } + + Component.onCompleted: { + Logger.d("BarExclusionZone", "Created for screen:", screen?.name) + Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating) + Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right) + Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight) + } +} diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml new file mode 100644 index 000000000..9d869b84f --- /dev/null +++ b/Modules/MainScreen/MainScreen.qml @@ -0,0 +1,536 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import "Backgrounds" as Backgrounds + +// All panels +import qs.Modules.Bar +import qs.Modules.Panels.Audio +import qs.Modules.Panels.Battery +import qs.Modules.Panels.Bluetooth +import qs.Modules.Panels.Calendar +import qs.Modules.Panels.ControlCenter +import qs.Modules.Panels.Launcher +import qs.Modules.Panels.NotificationHistory +import qs.Modules.Panels.SessionMenu +import qs.Modules.Panels.Settings +import qs.Modules.Panels.SetupWizard +import qs.Modules.Panels.Tray +import qs.Modules.Panels.Wallpaper +import qs.Modules.Panels.WiFi + + +/** + * MainScreen - Single PanelWindow per screen that manages all panels and the bar + */ +PanelWindow { + id: root + + // Expose panels as readonly property aliases + readonly property alias audioPanel: audioPanel + readonly property alias batteryPanel: batteryPanel + readonly property alias bluetoothPanel: bluetoothPanel + readonly property alias calendarPanel: calendarPanel + readonly property alias controlCenterPanel: controlCenterPanel + readonly property alias launcherPanel: launcherPanel + readonly property alias notificationHistoryPanel: notificationHistoryPanel + readonly property alias sessionMenuPanel: sessionMenuPanel + readonly property alias settingsPanel: settingsPanel + readonly property alias setupWizardPanel: setupWizardPanel + readonly property alias trayDropdownPanel: trayDropdownPanel + readonly property alias trayMenuPanel: trayMenuPanel + readonly property alias wallpaperPanel: wallpaperPanel + readonly property alias wifiPanel: wifiPanel + + Component.onCompleted: { + Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) + } + + // Debug: Log mask region changes + onMaskChanged: { + Logger.d("MainScreen", "Mask changed!") + Logger.d("MainScreen", " Bar region:", barLoader.item?.barRegion) + Logger.d("MainScreen", " Panel count:", panelsRepeater.count) + for (var i = 0; i < panelsRepeater.count; i++) { + var panelItem = panelsRepeater.itemAt(i)?.item + Logger.d("MainScreen", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion) + } + } + + // Wayland + // Always use Exclusive keyboard focus when a panel is open + // This ensures all keyboard shortcuts work reliably (Escape, etc.) + // The centralized shortcuts in this window handle delegation to panels + WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + WlrLayershell.layer: Settings.data.ui.panelsOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that + + anchors { + top: true + bottom: true + left: true + right: true + } + + // Desktop dimming when panels are open + property bool dimDesktop: Settings.data.general.dimDesktop + property bool isPanelOpen: PanelService.openedPanel !== null + color: { + if (dimDesktop && isPanelOpen) { + return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy) + } + return Color.transparent + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // Fully reactive mask system, make everything click-through except bar and open panels + mask: Region { + id: clickableMask + + // Cover entire window (everything is masked/click-through) + x: 0 + y: 0 + width: root.width + height: root.height + intersection: Intersection.Xor + + // Direct binding to Variants.instances plus additional regions + regions: panelRegions.instances.concat([barMaskRegion, backgroundMaskRegion]) + + // Bar region - subtract bar area from mask + Region { + id: barMaskRegion + property var barRegion: barLoader.item?.barRegion + + x: barRegion?.x ?? 0 + y: barRegion?.y ?? 0 + width: barRegion?.width ?? 0 + height: barRegion?.height ?? 0 + intersection: Intersection.Subtract + } + + // Background region for click-to-close - reactive sizing + Region { + id: backgroundMaskRegion + x: 0 + y: 0 + width: root.isPanelOpen ? root.width : 0 + height: root.isPanelOpen ? root.height : 0 + intersection: Intersection.Subtract + } + } + + // Variants for panel regions + Variants { + id: panelRegions + + // Model is the list of open panels (filters out closed ones) + model: [audioPanel, controlCenterPanel, calendarPanel].filter(function (p) { + return p && p.isPanelOpen + }) + + Region { + required property var modelData + property var panelBg: modelData.panelRegion + + // Direct bindings + x: panelBg?.x ?? 0 + y: panelBg?.y ?? 0 + width: panelBg?.width ?? 0 + height: panelBg?.height ?? 0 + intersection: Intersection.Subtract + } + } + + // Container for all UI elements + Item { + id: container + width: root.width + height: root.height + + // Unified backgrounds container / unified shadow system + // Renders all bar and panel backgrounds as ShapePaths within a single Shape + // This allows the shadow effect to apply to all backgrounds in one render pass + Backgrounds.AllBackgrounds { + id: unifiedBackgrounds + anchors.fill: parent + bar: barLoader.item?.barItem || null + windowRoot: root + z: 0 // Behind all content + } + + // Background MouseArea for closing panels when clicking outside + // Active whenever a panel is open - the mask ensures it only receives clicks when panel is open + MouseArea { + anchors.fill: parent + enabled: root.isPanelOpen + onClicked: { + if (PanelService.openedPanel) { + PanelService.openedPanel.close() + } + } + z: 0 // Behind panels and bar + } + + // --------------------------------------- + // All panels always exist + // --------------------------------------- + AudioPanel { + id: audioPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "audioPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(audioPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + BatteryPanel { + id: batteryPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "batteryPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(batteryPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + BluetoothPanel { + id: bluetoothPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "bluetoothPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(bluetoothPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + ControlCenterPanel { + id: controlCenterPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "controlCenterPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(controlCenterPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + CalendarPanel { + id: calendarPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "calendarPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(calendarPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + Launcher { + id: launcherPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "launcherPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(launcherPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + NotificationHistoryPanel { + id: notificationHistoryPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "notificationHistoryPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(notificationHistoryPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + SessionMenu { + id: sessionMenuPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "sessionMenuPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(sessionMenuPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + SettingsPanel { + id: settingsPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "settingsPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(settingsPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + SetupWizard { + id: setupWizardPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "setupWizardPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(setupWizardPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + TrayDropdownPanel { + id: trayDropdownPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "trayDropdownPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(trayDropdownPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + TrayMenuPanel { + id: trayMenuPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "trayMenuPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(trayDropdownPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + WallpaperPanel { + id: wallpaperPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "wallpaperPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(wallpaperPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + WiFiPanel { + id: wifiPanel + screen: root.screen + z: 50 + + Component.onCompleted: { + objectName = "wifiPanel-" + (screen?.name || "unknown") + PanelService.registerPanel(wifiPanel) + Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) + } + } + + // Bar (always on top - rendered last in tree, so naturally on top) + Loader { + id: barLoader + asynchronous: false + sourceComponent: Bar {} + // Keep bar loaded but hide it when BarService.isVisible is false + // This allows panels to remain accessible via IPC + visible: BarService.isVisible + + // Fill parent to provide dimensions for Bar to reference + anchors.fill: parent + + property ShellScreen screen: root.screen + + onLoaded: { + Logger.d("MainScreen", "Bar loaded:", item !== null) + if (item) { + Logger.d("MainScreen", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height) + // Bind screen to bar component (use binding for reactivity) + item.screen = Qt.binding(function () { + return barLoader.screen + }) + } + } + } + } + + // Centralized keyboard shortcuts - delegate to opened panel + // This ensures shortcuts work regardless of panel focus state + Shortcut { + sequence: "Escape" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) { + PanelService.openedPanel.onEscapePressed() + } else if (PanelService.openedPanel) { + PanelService.openedPanel.close() + } + } + } + + Shortcut { + sequence: "Tab" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) { + PanelService.openedPanel.onTabPressed() + } + } + } + + Shortcut { + sequence: "Shift+Tab" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) { + PanelService.openedPanel.onShiftTabPressed() + } + } + } + + Shortcut { + sequence: "Up" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) { + PanelService.openedPanel.onUpPressed() + } + } + } + + Shortcut { + sequence: "Down" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) { + PanelService.openedPanel.onDownPressed() + } + } + } + + Shortcut { + sequence: "Return" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { + PanelService.openedPanel.onReturnPressed() + } + } + } + + Shortcut { + sequence: "Enter" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { + PanelService.openedPanel.onReturnPressed() + } + } + } + + Shortcut { + sequence: "Home" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) { + PanelService.openedPanel.onHomePressed() + } + } + } + + Shortcut { + sequence: "End" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) { + PanelService.openedPanel.onEndPressed() + } + } + } + + Shortcut { + sequence: "PgUp" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) { + PanelService.openedPanel.onPageUpPressed() + } + } + } + + Shortcut { + sequence: "PgDown" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) { + PanelService.openedPanel.onPageDownPressed() + } + } + } + + Shortcut { + sequence: "Ctrl+J" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) { + PanelService.openedPanel.onCtrlJPressed() + } + } + } + + Shortcut { + sequence: "Ctrl+K" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) { + PanelService.openedPanel.onCtrlKPressed() + } + } + } + + Shortcut { + sequence: "Left" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) { + PanelService.openedPanel.onLeftPressed() + } + } + } + + Shortcut { + sequence: "Right" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) { + PanelService.openedPanel.onRightPressed() + } + } + } +} diff --git a/Modules/MainScreen/ShapeCornerHelper.qml b/Modules/MainScreen/ShapeCornerHelper.qml new file mode 100644 index 000000000..f16056a5d --- /dev/null +++ b/Modules/MainScreen/ShapeCornerHelper.qml @@ -0,0 +1,98 @@ +pragma Singleton + +import QtQuick +import QtQuick.Shapes + + +/** + * ShapeCornerHelper - Utility singleton for shape corner calculations + * + * Uses 4-state per-corner system for flexible corner rendering: + * - State -1: No radius (flat/square corner) + * - State 0: Normal (inner curve) + * - State 1: Horizontal inversion (outer curve on X-axis) + * - State 2: Vertical inversion (outer curve on Y-axis) + * + * The key technique: Using PathArc direction control (Clockwise vs Counterclockwise) + * combined with multipliers to create both inner and outer corner curves. + */ +QtObject { + id: root + + + /** + * Get X-axis multiplier for a corner state + * State 1 (horizontal invert) returns -1, others return 1 + */ + function getMultX(cornerState) { + return cornerState === 1 ? -1 : 1 + } + + + /** + * Get Y-axis multiplier for a corner state + * State 2 (vertical invert) returns -1, others return 1 + */ + function getMultY(cornerState) { + return cornerState === 2 ? -1 : 1 + } + + + /** + * Get PathArc direction for a corner based on its multipliers + * Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise + * This creates the outer curve effect for inverted corners + */ + function getArcDirection(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise + } + + + /** + * Convenience function to get arc direction directly from corner state + */ + function getArcDirectionFromState(cornerState) { + const multX = getMultX(cornerState) + const multY = getMultY(cornerState) + return getArcDirection(multX, multY) + } + + + /** + * Calculate the starting X position for a shape, accounting for top-left corner + * This is used to set ShapePath's startX position + */ + function getStartX(x, radius, topLeftState) { + return x + radius * getMultX(topLeftState) + } + + + /** + * Calculate the starting Y position for a shape + * In most cases this is just the y position, but can be adjusted for special cases + */ + function getStartY(y) { + return y + } + + + /** + * Get the "flattening" radius when shape dimensions are too small + * Prevents visual artifacts when radius exceeds dimensions + */ + function getFlattenedRadius(dimension, requestedRadius) { + if (dimension < requestedRadius * 2) { + return dimension / 2 + } + return requestedRadius + } + + + /** + * Check if a shape should use flattened corners + * Returns true if width or height is too small for the requested radius + */ + function shouldFlatten(width, height, radius) { + return width < radius * 2 || height < radius * 2 + } +} diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml new file mode 100644 index 000000000..1a184ea9a --- /dev/null +++ b/Modules/MainScreen/SmartPanel.qml @@ -0,0 +1,634 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + + +/** + * SmartPanel for use within MainScreen + */ +Item { + id: root + + // Screen property provided by MainScreen + property ShellScreen screen: null + + // Panel content: Text, icons, etc... + property Component panelContent: null + + // Panel size properties + property real preferredWidth: 700 + property real preferredHeight: 900 + property real preferredWidthRatio + property real preferredHeightRatio + property color panelBackgroundColor: Color.mSurface + property color panelBorderColor: Color.mOutline + property var buttonItem: null + + // Anchoring properties + property bool panelAnchorHorizontalCenter: false + property bool panelAnchorVerticalCenter: false + property bool panelAnchorTop: false + property bool panelAnchorBottom: false + property bool panelAnchorLeft: false + property bool panelAnchorRight: false + + // Button position properties + property bool useButtonPosition: false + property point buttonPosition: Qt.point(0, 0) + property int buttonWidth: 0 + property int buttonHeight: 0 + + // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap + property real edgeSnapDistance: 50 + + // Track whether panel is open + property bool isPanelOpen: false + + // Track actual visibility (delayed until content is loaded and sized) + property bool isPanelVisible: false + + // Keyboard event handlers - override these in specific panels to handle shortcuts + // These are called from MainScreen's centralized shortcuts + function onEscapePressed() { + close() + } + function onTabPressed() {} + function onShiftTabPressed() {} + function onUpPressed() {} + function onDownPressed() {} + function onLeftPressed() {} + function onRightPressed() {} + function onReturnPressed() {} + function onHomePressed() {} + function onEndPressed() {} + function onPageUpPressed() {} + function onPageDownPressed() {} + function onCtrlJPressed() {} + function onCtrlKPressed() {} + + // Expose panel region for click-through mask + readonly property var panelRegion: panelContent.maskRegion + + readonly property string barPosition: Settings.data.bar.position + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + + // Helper to detect if any anchor is explicitly set + readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight + readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom + + // Effective anchor properties (depend on couldAttach) + // These are true when: + // 1. Explicitly anchored, OR + // 2. Using button position and bar is on that edge, OR + // 3. Attached to bar with no explicit anchors (default centering behavior) + readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") || (panelContent.couldAttach && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical) + readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (panelContent.couldAttach && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical) + readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (panelContent.couldAttach && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical) + readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (panelContent.couldAttach && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical) + + signal opened + signal closed + + // Panel visibility and sizing + visible: isPanelVisible + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + + // Panel control functions + function toggle(buttonItem, buttonName) { + if (!isPanelOpen) { + open(buttonItem, buttonName) + } else { + close() + } + } + + function open(buttonItem, buttonName) { + if (!buttonItem && buttonName) { + buttonItem = BarService.lookupWidget(buttonName, screen.name) + } + + if (buttonItem) { + root.buttonItem = buttonItem + // Map button position to screen coordinates + var buttonPos = buttonItem.mapToItem(null, 0, 0) + root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y) + root.buttonWidth = buttonItem.width + root.buttonHeight = buttonItem.height + root.useButtonPosition = true + } else { + // No button provided: reset button position mode + root.buttonItem = null + root.useButtonPosition = false + } + + // Set isPanelOpen to trigger content loading, but don't show yet + isPanelOpen = true + + // Notify PanelService + PanelService.willOpenPanel(root) + + // Position and visibility will be set by Loader.onLoaded + // This ensures no flicker from default size to content size + } + + function close() { + // Set visibility to false - state transition will animate + isPanelVisible = false + isPanelOpen = false + PanelService.closedPanel(root) + closed() + + Logger.d("SmartPanel", "Closing panel", objectName) + } + + function setPosition() { + // Calculate panel dimensions first (needed for positioning) + var w + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) { + w = contentLoader.item.contentPreferredWidth + } // Priority 2: Ratio-based size + else if (root.preferredWidthRatio !== undefined) { + w = Math.round(Math.max((root.width || 1920) * root.preferredWidthRatio, root.preferredWidth)) + } // Priority 3: Static preferred width + else { + w = root.preferredWidth + } + var panelWidth = Math.min(w, (root.width || 1920) - Style.marginL * 2) + + var h + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) { + h = contentLoader.item.contentPreferredHeight + } // Priority 2: Ratio-based size + else if (root.preferredHeightRatio !== undefined) { + h = Math.round(Math.max((root.height || 1080) * root.preferredHeightRatio, root.preferredHeight)) + } // Priority 3: Static preferred height + else { + h = root.preferredHeight + } + var panelHeight = Math.min(h, (root.height || 1080) - Style.barHeight - Style.marginL * 2) + + // Update panelBackground size + panelBackground.width = panelWidth + panelBackground.height = panelHeight + + // Calculate position + var calculatedX + var calculatedY + + // ===== X POSITIONING ===== + if (root.useButtonPosition && root.width > 0 && panelWidth > 0) { + if (root.barIsVertical) { + // For vertical bars + if (panelContent.couldAttach) { + // Attached panels: align with bar edge (left or right side) + if (root.barPosition === "left") { + var leftBarEdge = root.barMarginH + Style.barHeight + calculatedX = leftBarEdge + } else { + // right + var rightBarEdge = root.width - root.barMarginH - Style.barHeight + calculatedX = rightBarEdge - panelWidth + } + } else { + // Detached panels: center on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2 + var minX = Style.marginL + var maxX = root.width - panelWidth - Style.marginL + + // Account for vertical bar taking up space + if (root.barPosition === "left") { + minX = root.barMarginH + Style.barHeight + Style.marginL + } else if (root.barPosition === "right") { + maxX = root.width - root.barMarginH - Style.barHeight - panelWidth - Style.marginL + } + + panelX = Math.max(minX, Math.min(panelX, maxX)) + calculatedX = panelX + } + } else { + // For horizontal bars, center panel on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2 + if (panelContent.couldAttach) { + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = root.width - root.barMarginH - cornerInset + panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - panelWidth)) + } else { + panelX = Math.max(Style.marginL, Math.min(panelX, root.width - panelWidth - Style.marginL)) + } + calculatedX = panelX + } + } else { + // Standard anchor positioning + if (root.panelAnchorHorizontalCenter) { + if (root.barIsVertical) { + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = root.width - availableStart + calculatedX = availableStart + (availableWidth - panelWidth) / 2 + } else if (root.barPosition === "right") { + var availableWidth = root.width - root.barMarginH - Style.barHeight + calculatedX = (availableWidth - panelWidth) / 2 + } else { + calculatedX = (root.width - panelWidth) / 2 + } + } else { + calculatedX = (root.width - panelWidth) / 2 + } + } else if (root.effectivePanelAnchorRight) { + if (panelContent.couldAttach && root.barIsVertical && root.barPosition === "right") { + var rightBarEdge = root.width - root.barMarginH - Style.barHeight + calculatedX = rightBarEdge - panelWidth + } else if (panelContent.couldAttach) { + calculatedX = root.width - panelWidth + } else { + calculatedX = root.width - panelWidth - Style.marginL + } + } else if (root.effectivePanelAnchorLeft) { + if (panelContent.couldAttach && root.barIsVertical && root.barPosition === "left") { + var leftBarEdge = root.barMarginH + Style.barHeight + calculatedX = leftBarEdge + } else if (panelContent.couldAttach) { + calculatedX = 0 + } else { + calculatedX = Style.marginL + } + } else { + // No explicit anchor: default to centering on bar + if (root.barIsVertical) { + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = root.width - availableStart - Style.marginL + calculatedX = availableStart + (availableWidth - panelWidth) / 2 + } else { + var availableWidth = root.width - root.barMarginH - Style.barHeight - Style.marginL + calculatedX = Style.marginL + (availableWidth - panelWidth) / 2 + } + } else { + if (panelContent.couldAttach) { + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = root.width - root.barMarginH - cornerInset + var centeredX = (root.width - panelWidth) / 2 + calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - panelWidth)) + } else { + calculatedX = (root.width - panelWidth) / 2 + } + } + } + } + + // Edge snapping for X + if (panelContent.couldAttach && !root.barFloating && root.width > 0 && panelWidth > 0) { + var leftEdgePos = root.barMarginH + if (root.barPosition === "left") { + leftEdgePos = root.barMarginH + Style.barHeight + } + + var rightEdgePos = root.width - root.barMarginH - panelWidth + if (root.barPosition === "right") { + rightEdgePos = root.width - root.barMarginH - Style.barHeight - panelWidth + } + + if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { + calculatedX = leftEdgePos + } else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) { + calculatedX = rightEdgePos + } + } + + // ===== Y POSITIONING ===== + if (root.useButtonPosition && root.height > 0 && panelHeight > 0) { + if (root.barPosition === "top") { + var topBarEdge = root.barMarginV + Style.barHeight + if (panelContent.couldAttach) { + calculatedY = topBarEdge + } else { + calculatedY = topBarEdge + Style.marginM + } + } else if (root.barPosition === "bottom") { + var bottomBarEdge = root.height - root.barMarginV - Style.barHeight + if (panelContent.couldAttach) { + calculatedY = bottomBarEdge - panelHeight + } else { + calculatedY = bottomBarEdge - panelHeight - Style.marginM + } + } else if (root.barIsVertical) { + var panelY = root.buttonPosition.y + root.buttonHeight / 2 - panelHeight / 2 + var extraPadding = (panelContent.couldAttach && root.barFloating) ? Style.radiusL : 0 + if (panelContent.couldAttach) { + var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0) + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = root.height - root.barMarginV - cornerInset + panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - panelHeight)) + } else { + panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, root.height - panelHeight - Style.marginL - extraPadding)) + } + calculatedY = panelY + } + } else { + // Standard anchor positioning + var barOffset = 0 + if (!panelContent.couldAttach) { + if (root.barPosition === "top") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM + } else if (root.barPosition === "bottom") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM + } + } else { + if (root.effectivePanelAnchorTop && root.barPosition === "top") { + calculatedY = root.barMarginV + Style.barHeight + } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { + calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight + } else if (!root.hasExplicitVerticalAnchor) { + if (root.barPosition === "top") { + calculatedY = root.barMarginV + Style.barHeight + } else if (root.barPosition === "bottom") { + calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight + } + } + } + + if (calculatedY === undefined) { + if (root.panelAnchorVerticalCenter) { + if (!root.barIsVertical) { + if (root.barPosition === "top") { + var availableStart = root.barMarginV + Style.barHeight + var availableHeight = root.height - availableStart + calculatedY = availableStart + (availableHeight - panelHeight) / 2 + } else if (root.barPosition === "bottom") { + var availableHeight = root.height - root.barMarginV - Style.barHeight + calculatedY = (availableHeight - panelHeight) / 2 + } else { + calculatedY = (root.height - panelHeight) / 2 + } + } else { + calculatedY = (root.height - panelHeight) / 2 + } + } else if (root.effectivePanelAnchorTop) { + if (panelContent.couldAttach) { + calculatedY = 0 + } else { + var topBarOffset = (root.barPosition === "top") ? barOffset : 0 + calculatedY = topBarOffset + Style.marginL + } + } else if (root.effectivePanelAnchorBottom) { + if (panelContent.couldAttach) { + calculatedY = root.height - panelHeight + } else { + var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0 + calculatedY = root.height - panelHeight - bottomBarOffset - Style.marginL + } + } else { + if (root.barIsVertical) { + if (panelContent.couldAttach) { + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = root.height - root.barMarginV - cornerInset + var centeredY = (root.height - panelHeight) / 2 + calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - panelHeight)) + } else { + calculatedY = (root.height - panelHeight) / 2 + } + } else { + if (panelContent.couldAttach && !root.barIsVertical) { + if (root.barPosition === "top") { + calculatedY = root.barMarginV + Style.barHeight + } else if (root.barPosition === "bottom") { + calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight + } + } else { + if (root.barPosition === "top") { + calculatedY = barOffset + Style.marginL + } else if (root.barPosition === "bottom") { + calculatedY = Style.marginL + } else { + calculatedY = Style.marginL + } + } + } + } + } + } + + // Edge snapping for Y + if (panelContent.couldAttach && !root.barFloating && root.height > 0 && panelHeight > 0) { + var topEdgePos = root.barMarginV + if (root.barPosition === "top") { + topEdgePos = root.barMarginV + Style.barHeight + } + + var bottomEdgePos = root.height - root.barMarginV - panelHeight + if (root.barPosition === "bottom") { + bottomEdgePos = root.height - root.barMarginV - Style.barHeight - panelHeight + } + + if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { + calculatedY = topEdgePos + } else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { + calculatedY = bottomEdgePos + } + } + + // Apply calculated positions + panelBackground.x = calculatedX + panelBackground.y = calculatedY + + Logger.d("SmartPanel", "Position calculated:", calculatedX, calculatedY) + Logger.d("SmartPanel", " Panel size:", panelWidth, "x", panelHeight) + } + + // Watch for changes in content-driven sizes and update position + Connections { + target: contentLoader.item + ignoreUnknownSignals: true + + function onContentPreferredWidthChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition() + } + } + + function onContentPreferredHeightChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition() + } + } + } + + // Simple opacity-based animation + opacity: isPanelVisible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // ------------------------------------------------ + // Panel Content + Item { + id: panelContent + anchors.fill: parent + + // Screen-dependent attachment properties + readonly property bool couldAttach: Settings.data.ui.panelsAttachedToBar + readonly property bool couldAttachToBar: { + if (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity < 1.0) { + return false + } + + // A panel can only be attached to a bar if there is a bar on that screen + var monitors = Settings.data.bar.monitors || [] + var result = monitors.length === 0 || monitors.includes(root.screen?.name || "") + return result + } + + // Edge detection - detect if panel is touching screen edges + readonly property bool touchingLeftEdge: couldAttach && panelBackground.x <= 1 + readonly property bool touchingRightEdge: couldAttach && (panelBackground.x + panelBackground.width) >= (root.width - 1) + readonly property bool touchingTopEdge: couldAttach && panelBackground.y <= 1 + readonly property bool touchingBottomEdge: couldAttach && (panelBackground.y + panelBackground.height) >= (root.height - 1) + + // Bar edge detection - detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints) + readonly property bool touchingTopBar: couldAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.y - (root.barMarginV + Style.barHeight)) <= 1 + readonly property bool touchingBottomBar: couldAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((panelBackground.y + panelBackground.height) - (root.height - root.barMarginV - Style.barHeight)) <= 1 + readonly property bool touchingLeftBar: couldAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(panelBackground.x - (root.barMarginH + Style.barHeight)) <= 1 + readonly property bool touchingRightBar: couldAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((panelBackground.x + panelBackground.width) - (root.width - root.barMarginH - Style.barHeight)) <= 1 + + // Expose panelBackground for mask region + property alias maskRegion: panelBackground + + // The actual panel background - provides geometry for PanelBackground rendering + Item { + id: panelBackground + x: root.x + y: root.y + width: root.preferredWidth + height: root.preferredHeight + + // Corner states for PanelBackground to read + // State -1: No radius (flat/square corner) + // State 0: Normal (inner curve) + // State 1: Horizontal inversion (outer curve on X-axis) + // State 2: Vertical inversion (outer curve on Y-axis) + + // Smart corner state calculation based on bar attachment and edge touching + property int topLeftCornerState: { + // Bar attachment: only attach to bar if bar opacity >= 1.0 (no color clash) + var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + // Also detect when panel touches bar edge (e.g., centered panel that's too tall) + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingLeftBar + // Screen edge contact: can attach to screen edges even if bar opacity < 1.0 + // For horizontal bars: invert when touching left/right edges + // For vertical bars: invert when touching top/bottom edges + var edgeInverted = panelContent.couldAttach && ((panelContent.touchingLeftEdge && !root.barIsVertical) || (panelContent.touchingTopEdge && root.barIsVertical)) + // Also invert when touching screen edge opposite to bar (e.g., bottom edge when bar is at top) + var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top") + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + // Determine direction: horizontal bars → horizontal curves, vertical bars → vertical curves + // Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves + if (panelContent.touchingLeftEdge && !root.barIsVertical) + return 2 // Vertical inversion + if (panelContent.touchingTopEdge && root.barIsVertical) + return 1 // Horizontal inversion + return root.barIsVertical ? 2 : 1 + } + return 0 // Normal corner + } + + property int topRightCornerState: { + var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingRightBar + var edgeInverted = panelContent.couldAttach && ((panelContent.touchingRightEdge && !root.barIsVertical) || (panelContent.touchingTopEdge && root.barIsVertical)) + var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top") + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + if (panelContent.touchingRightEdge && !root.barIsVertical) + return 2 + if (panelContent.touchingTopEdge && root.barIsVertical) + return 1 + return root.barIsVertical ? 2 : 1 + } + return 0 + } + + property int bottomLeftCornerState: { + var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingLeftBar + var edgeInverted = panelContent.couldAttach && ((panelContent.touchingLeftEdge && !root.barIsVertical) || (panelContent.touchingBottomEdge && root.barIsVertical)) + var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom") + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + if (panelContent.touchingLeftEdge && !root.barIsVertical) + return 2 + if (panelContent.touchingBottomEdge && root.barIsVertical) + return 1 + return root.barIsVertical ? 2 : 1 + } + return 0 + } + + property int bottomRightCornerState: { + var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingRightBar + var edgeInverted = panelContent.couldAttach && ((panelContent.touchingRightEdge && !root.barIsVertical) || (panelContent.touchingBottomEdge && root.barIsVertical)) + var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom") + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + if (panelContent.touchingRightEdge && !root.barIsVertical) + return 2 + if (panelContent.touchingBottomEdge && root.barIsVertical) + return 1 + return root.barIsVertical ? 2 : 1 + } + return 0 + } + + // MouseArea to catch clicks on the panel and prevent them from reaching the background + // This prevents closing the panel when clicking inside it + MouseArea { + anchors.fill: parent + z: -1 // Behind content, but on the panel background + onClicked: { + + // Accept and ignore - prevents propagation to background + } + } + } + + // Panel top content: Text, icons, etc... + Loader { + id: contentLoader + active: isPanelOpen + x: panelBackground.x + y: panelBackground.y + width: panelBackground.width + height: panelBackground.height + sourceComponent: root.panelContent + + // When content finishes loading, make visible then calculate position + onLoaded: { + // Make panel visible FIRST so corner state bindings have valid context + // This happens in the same frame, so no visual flicker + isPanelVisible = true + + // Calculate position with content-driven sizes if available + // When position updates, corner state bindings re-evaluate with panel visible + setPosition() + + // Emit opened signal + opened() + } + } + } +} diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml deleted file mode 100644 index 3d7c61b15..000000000 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ /dev/null @@ -1,329 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Notifications -import qs.Commons -import qs.Services -import qs.Widgets - -// Notification History panel -NPanel { - id: root - - preferredWidth: 380 * Style.uiScaleRatio - preferredHeight: 480 * Style.uiScaleRatio - - onOpened: function () { - NotificationService.updateLastSeenTs() - } - - panelContent: Rectangle { - id: notificationRect - color: Color.transparent - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM - - // Header section - NBox { - Layout.fillWidth: true - implicitHeight: headerRow.implicitHeight + (Style.marginM * 2) - - RowLayout { - id: headerRow - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM - - NIcon { - icon: "bell" - pointSize: Style.fontSizeXXL - color: Color.mPrimary - } - - NText { - text: I18n.tr("notifications.panel.title") - pointSize: Style.fontSizeL - font.weight: Style.fontWeightBold - color: Color.mOnSurface - Layout.fillWidth: true - } - - NIconButton { - icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" - tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled") - baseSize: Style.baseWidgetSize * 0.8 - onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb - } - - NIconButton { - icon: "trash" - tooltipText: I18n.tr("tooltips.clear-history") - baseSize: Style.baseWidgetSize * 0.8 - onClicked: { - NotificationService.clearHistory() - // Close panel as there is nothing more to see. - root.close() - } - } - - NIconButton { - icon: "close" - tooltipText: I18n.tr("tooltips.close") - baseSize: Style.baseWidgetSize * 0.8 - onClicked: root.close() - } - } - } - - // Empty state when no notifications - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - visible: NotificationService.historyList.count === 0 - spacing: Style.marginL - - Item { - Layout.fillHeight: true - } - - NIcon { - icon: "bell-off" - pointSize: 64 - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: I18n.tr("notifications.panel.no-notifications") - pointSize: Style.fontSizeL - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: I18n.tr("notifications.panel.description") - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - } - - Item { - Layout.fillHeight: true - } - } - - // Notification list - NListView { - id: notificationList - Layout.fillWidth: true - Layout.fillHeight: true - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AsNeeded - - model: NotificationService.historyList - spacing: Style.marginM - clip: true - boundsBehavior: Flickable.StopAtBounds - visible: NotificationService.historyList.count > 0 - - // Track which notification is expanded - property string expandedId: "" - - delegate: Rectangle { - property string notificationId: model.id - property bool isExpanded: notificationList.expandedId === notificationId - - width: notificationList.width - height: notificationLayout.implicitHeight + (Style.marginM * 2) - radius: Style.radiusM - color: Color.mSurfaceVariant - border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) - border.width: Style.borderS - - Behavior on height { - enabled: !Settings.data.general.animationDisabled - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.InOutQuad - } - } - - // Smooth color transition on hover - Behavior on color { - enabled: !Settings.data.general.animationDisabled - ColorAnimation { - duration: Style.animationFast - } - } - - // Click to expand/collapse - MouseArea { - anchors.fill: parent - // Don't capture clicks on the delete button - anchors.rightMargin: 48 - enabled: (summaryText.truncated || bodyText.truncated) - onClicked: { - if (notificationList.expandedId === notificationId) { - notificationList.expandedId = "" - } else { - notificationList.expandedId = notificationId - } - } - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - } - - RowLayout { - id: notificationLayout - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM - - ColumnLayout { - NImageCircled { - Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio) - Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio) - Layout.alignment: Qt.AlignTop - Layout.topMargin: 20 - imagePath: model.cachedImage || model.originalImage || "" - borderColor: Color.transparent - borderWidth: 0 - fallbackIcon: "bell" - fallbackIconSize: 24 - } - Item { - Layout.fillHeight: true - } - } - - // Notification content column - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Style.marginXS - Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6) - - // Header row with app name and timestamp - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS - - // Urgency indicator - Rectangle { - Layout.preferredWidth: 6 - Layout.preferredHeight: 6 - Layout.alignment: Qt.AlignVCenter - radius: 3 - visible: model.urgency !== 1 - color: { - if (model.urgency === 2) - return Color.mError - else if (model.urgency === 0) - return Color.mOnSurfaceVariant - else - return Color.transparent - } - } - - NText { - text: model.appName || "Unknown App" - pointSize: Style.fontSizeXS - color: Color.mSecondary - } - - NText { - text: Time.formatRelativeTime(model.timestamp) - pointSize: Style.fontSizeXS - color: Color.mSecondary - } - - Item { - Layout.fillWidth: true - } - } - - // Summary - NText { - id: summaryText - text: model.summary || I18n.tr("general.no-summary") - pointSize: Style.fontSizeM - font.weight: Font.Medium - color: Color.mOnSurface - textFormat: Text.PlainText - wrapMode: Text.Wrap - Layout.fillWidth: true - maximumLineCount: isExpanded ? 999 : 2 - elide: Text.ElideRight - } - - // Body - NText { - id: bodyText - text: model.body || "" - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - textFormat: Text.PlainText - wrapMode: Text.Wrap - Layout.fillWidth: true - maximumLineCount: isExpanded ? 999 : 3 - elide: Text.ElideRight - visible: text.length > 0 - } - - // Spacer for expand indicator - Item { - Layout.fillWidth: true - Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0 - } - - // Expand indicator - RowLayout { - Layout.fillWidth: true - visible: !isExpanded && (summaryText.truncated || bodyText.truncated) - spacing: Style.marginXS - - Item { - Layout.fillWidth: true - } - - NText { - text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand" - pointSize: Style.fontSizeXS - color: Color.mPrimary - font.weight: Font.Medium - } - - NIcon { - icon: "chevron-down" - pointSize: Style.fontSizeS - color: Color.mPrimary - } - } - } - - // Delete button - NIconButton { - icon: "trash" - tooltipText: I18n.tr("tooltips.delete-notification") - baseSize: Style.baseWidgetSize * 0.7 - Layout.alignment: Qt.AlignTop - - onClicked: { - // Remove from history using the service API - NotificationService.removeFromHistory(notificationId) - } - } - } - } - } - } - } -} diff --git a/Modules/Bar/Audio/AudioPanel.qml b/Modules/Panels/Audio/AudioPanel.qml similarity index 99% rename from Modules/Bar/Audio/AudioPanel.qml rename to Modules/Panels/Audio/AudioPanel.qml index 17e92c625..6b03f7e70 100644 --- a/Modules/Bar/Audio/AudioPanel.qml +++ b/Modules/Panels/Audio/AudioPanel.qml @@ -6,8 +6,9 @@ import Quickshell.Services.Pipewire import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root property real localOutputVolume: AudioService.volume || 0 @@ -53,9 +54,7 @@ NPanel { } } - panelContent: Rectangle { - color: Color.transparent - + panelContent: Item { // Use implicitHeight from content + margins to avoid binding loops property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2 diff --git a/Modules/Bar/Battery/BatteryPanel.qml b/Modules/Panels/Battery/BatteryPanel.qml similarity index 98% rename from Modules/Bar/Battery/BatteryPanel.qml rename to Modules/Panels/Battery/BatteryPanel.qml index cf2d5c1e0..02803865c 100644 --- a/Modules/Bar/Battery/BatteryPanel.qml +++ b/Modules/Panels/Battery/BatteryPanel.qml @@ -6,8 +6,9 @@ import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root property var optionsModel: [] diff --git a/Modules/Bar/Bluetooth/BluetoothDevicesList.qml b/Modules/Panels/Bluetooth/BluetoothDevicesList.qml similarity index 100% rename from Modules/Bar/Bluetooth/BluetoothDevicesList.qml rename to Modules/Panels/Bluetooth/BluetoothDevicesList.qml diff --git a/Modules/Bar/Bluetooth/BluetoothPanel.qml b/Modules/Panels/Bluetooth/BluetoothPanel.qml similarity index 99% rename from Modules/Bar/Bluetooth/BluetoothPanel.qml rename to Modules/Panels/Bluetooth/BluetoothPanel.qml index 09a45d59b..213f09753 100644 --- a/Modules/Bar/Bluetooth/BluetoothPanel.qml +++ b/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -7,8 +7,9 @@ import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root preferredWidth: 420 * Style.uiScaleRatio diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Panels/Calendar/CalendarPanel.qml similarity index 99% rename from Modules/Bar/Calendar/CalendarPanel.qml rename to Modules/Panels/Calendar/CalendarPanel.qml index dddfd8be1..e21369c66 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Panels/Calendar/CalendarPanel.qml @@ -4,14 +4,14 @@ import QtQuick.Layouts import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Modules.ControlCenter.Cards import qs.Services import qs.Widgets +import qs.Modules.MainScreen +import qs.Modules.Panels.ControlCenter.Cards -NPanel { +SmartPanel { id: root - property ShellScreen screen readonly property var now: Time.now preferredWidth: 500 * Style.uiScaleRatio @@ -32,9 +32,12 @@ NPanel { panelContent: Item { anchors.fill: parent - // Dynamic sizing properties that NPanel will bind to + // Dynamic sizing properties that SmartPanel will bind to property real contentPreferredWidth: (Settings.data.location.showWeekNumberInCalendar ? 400 : 380) * Style.uiScaleRatio + // Use implicitHeight from content + margins to avoid binding loops + property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2 + property real calendarGridHeight: { // Calculate number of weeks in the calendar grid const numWeeks = grid.daysModel ? Math.ceil(grid.daysModel.length / 7) : 5 @@ -44,10 +47,6 @@ NPanel { return numWeeks * rowHeight } - - // Use implicitHeight from content + margins to avoid binding loops - property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2 - ColumnLayout { id: content anchors.fill: parent diff --git a/Modules/ControlCenter/Cards/AudioCard.qml b/Modules/Panels/ControlCenter/Cards/AudioCard.qml similarity index 100% rename from Modules/ControlCenter/Cards/AudioCard.qml rename to Modules/Panels/ControlCenter/Cards/AudioCard.qml diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/Panels/ControlCenter/Cards/MediaCard.qml similarity index 99% rename from Modules/ControlCenter/Cards/MediaCard.qml rename to Modules/Panels/ControlCenter/Cards/MediaCard.qml index cb825e2de..580e58835 100644 --- a/Modules/ControlCenter/Cards/MediaCard.qml +++ b/Modules/Panels/ControlCenter/Cards/MediaCard.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects import Quickshell -import qs.Modules.AudioSpectrum +import qs.Widgets.AudioSpectrum import qs.Commons import qs.Services import qs.Widgets diff --git a/Modules/ControlCenter/Cards/ProfileCard.qml b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml similarity index 97% rename from Modules/ControlCenter/Cards/ProfileCard.qml rename to Modules/Panels/ControlCenter/Cards/ProfileCard.qml index 408c3f19a..a8e044849 100644 --- a/Modules/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml @@ -4,11 +4,11 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets -import qs.Modules.Settings -import qs.Modules.ControlCenter import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.Panels.ControlCenter.Cards +import qs.Modules.Panels.Settings // Header card with avatar, user and quick actions NBox { diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml similarity index 96% rename from Modules/ControlCenter/Cards/ShortcutsCard.qml rename to Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml index 3c9cefee0..5e7c7ad67 100644 --- a/Modules/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml @@ -5,8 +5,8 @@ import Quickshell import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.ControlCenter -import qs.Modules.ControlCenter.Cards +import qs.Modules.Panels.ControlCenter +import qs.Modules.Panels.ControlCenter.Cards RowLayout { Layout.fillWidth: true diff --git a/Modules/ControlCenter/Cards/SystemMonitorCard.qml b/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml similarity index 95% rename from Modules/ControlCenter/Cards/SystemMonitorCard.qml rename to Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml index 390d4ee3c..fc94aa97c 100644 --- a/Modules/ControlCenter/Cards/SystemMonitorCard.qml +++ b/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml @@ -46,7 +46,7 @@ NBox { Layout.alignment: Qt.AlignHCenter } NCircleStat { - value: SystemStatService.diskPercents["/"] + value: SystemStatService.diskPercents["/"] ?? 0 icon: "storage" flat: true contentScale: 0.8 diff --git a/Modules/ControlCenter/Cards/WeatherCard.qml b/Modules/Panels/ControlCenter/Cards/WeatherCard.qml similarity index 100% rename from Modules/ControlCenter/Cards/WeatherCard.qml rename to Modules/Panels/ControlCenter/Cards/WeatherCard.qml diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/Panels/ControlCenter/ControlCenterPanel.qml similarity index 79% rename from Modules/ControlCenter/ControlCenterPanel.qml rename to Modules/Panels/ControlCenter/ControlCenterPanel.qml index 9f570d915..baff73d85 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/Panels/ControlCenter/ControlCenterPanel.qml @@ -2,14 +2,18 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import qs.Modules.ControlCenter.Cards import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen +import qs.Modules.Panels.ControlCenter.Cards -NPanel { +SmartPanel { id: root + panelAnchorHorizontalCenter: true + panelAnchorVerticalCenter: true + preferredWidth: Math.round(460 * Style.uiScaleRatio) preferredHeight: { var height = 0 @@ -57,15 +61,14 @@ NPanel { } // When position is "close_to_bar_button" but there's no bar, fall back to center - readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen - - panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center")) - panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center" - panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left") - panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right") - panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_") - panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_") + // readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen + // panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center")) + // panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center" + // panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left") + // panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right") + // panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_") + // panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_") readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio) readonly property int shortcutsHeight: Math.round(52 * Style.uiScaleRatio) readonly property int audioHeight: Math.round(60 * Style.uiScaleRatio) diff --git a/Modules/ControlCenter/ControlCenterWidgetLoader.qml b/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml similarity index 100% rename from Modules/ControlCenter/ControlCenterWidgetLoader.qml rename to Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml similarity index 100% rename from Modules/ControlCenter/Widgets/Bluetooth.qml rename to Modules/Panels/ControlCenter/Widgets/Bluetooth.qml diff --git a/Modules/ControlCenter/Widgets/CustomButton.qml b/Modules/Panels/ControlCenter/Widgets/CustomButton.qml similarity index 100% rename from Modules/ControlCenter/Widgets/CustomButton.qml rename to Modules/Panels/ControlCenter/Widgets/CustomButton.qml diff --git a/Modules/ControlCenter/Widgets/KeepAwake.qml b/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml similarity index 100% rename from Modules/ControlCenter/Widgets/KeepAwake.qml rename to Modules/Panels/ControlCenter/Widgets/KeepAwake.qml diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/Panels/ControlCenter/Widgets/NightLight.qml similarity index 96% rename from Modules/ControlCenter/Widgets/NightLight.qml rename to Modules/Panels/ControlCenter/Widgets/NightLight.qml index bccea9a64..c59126ade 100644 --- a/Modules/ControlCenter/Widgets/NightLight.qml +++ b/Modules/Panels/ControlCenter/Widgets/NightLight.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets diff --git a/Modules/ControlCenter/Widgets/Notifications.qml b/Modules/Panels/ControlCenter/Widgets/Notifications.qml similarity index 100% rename from Modules/ControlCenter/Widgets/Notifications.qml rename to Modules/Panels/ControlCenter/Widgets/Notifications.qml diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml similarity index 100% rename from Modules/ControlCenter/Widgets/PowerProfile.qml rename to Modules/Panels/ControlCenter/Widgets/PowerProfile.qml diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml similarity index 100% rename from Modules/ControlCenter/Widgets/ScreenRecorder.qml rename to Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml similarity index 100% rename from Modules/ControlCenter/Widgets/WallpaperSelector.qml rename to Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/Panels/ControlCenter/Widgets/WiFi.qml similarity index 100% rename from Modules/ControlCenter/Widgets/WiFi.qml rename to Modules/Panels/ControlCenter/Widgets/WiFi.qml diff --git a/Modules/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml similarity index 94% rename from Modules/Launcher/Launcher.qml rename to Modules/Panels/Launcher/Launcher.qml index 928d93f83..3374e737a 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -6,8 +6,10 @@ import Quickshell.Widgets import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen +import "Plugins" -NPanel { +SmartPanel { id: root // Panel configuration @@ -49,7 +51,7 @@ NPanel { readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6) readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) - // Override keyboard handlers from NPanel for navigation + // Override keyboard handlers from SmartPanel for navigation function onTabPressed() { selectNextWrapped() } @@ -151,14 +153,14 @@ NPanel { resultsReady = false ignoreMouseHover = true - // Notify plugins - for (let plugin of plugins) { - if (plugin.onOpened) - plugin.onOpened() - } - updateResults() - + // Notify plugins and update results + // Use Qt.callLater to ensure plugins are registered (Component.onCompleted runs first) Qt.callLater(() => { + for (let plugin of plugins) { + if (plugin.onOpened) + plugin.onOpened() + } + updateResults() resultsReady = true }) } @@ -175,33 +177,28 @@ NPanel { } } - // Load plugins - Component.onCompleted: { - // Load applications plugin - const appsPlugin = Qt.createComponent("Plugins/ApplicationsPlugin.qml").createObject(this) - if (appsPlugin) { - registerPlugin(appsPlugin) + // Plugin components - declared inline so imports work correctly + ApplicationsPlugin { + id: appsPlugin + Component.onCompleted: { + registerPlugin(this) Logger.d("Launcher", "Registered: ApplicationsPlugin") - } else { - Logger.e("Launcher", "Failed to load ApplicationsPlugin") } + } - // Load calculator plugin - const calcPlugin = Qt.createComponent("Plugins/CalculatorPlugin.qml").createObject(this) - if (calcPlugin) { - registerPlugin(calcPlugin) + CalculatorPlugin { + id: calcPlugin + Component.onCompleted: { + registerPlugin(this) Logger.d("Launcher", "Registered: CalculatorPlugin") - } else { - Logger.e("Launcher", "Failed to load CalculatorPlugin") } + } - // Load clipboard history plugin - const clipboardPlugin = Qt.createComponent("Plugins/ClipboardPlugin.qml").createObject(this) - if (clipboardPlugin) { - registerPlugin(clipboardPlugin) + ClipboardPlugin { + id: clipPlugin + Component.onCompleted: { + registerPlugin(this) Logger.d("Launcher", "Registered: ClipboardPlugin") - } else { - Logger.e("Launcher", "Failed to load ClipboardPlugin") } } diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml similarity index 98% rename from Modules/Launcher/Plugins/ApplicationsPlugin.qml rename to Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml index 2e07442c7..8add1460d 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import qs.Commons import qs.Services -import "../../../Helpers/FuzzySort.js" as Fuzzysort +import "../../../../Helpers/FuzzySort.js" as Fuzzysort Item { property var launcher: null @@ -194,7 +194,7 @@ Item { "icon": app.icon || "application-x-executable", "isImage": false, "onActivate": function () { - // Close the launcher/NPanel immediately without any animations. + // Close the launcher/SmartPanel immediately without any animations. // Ensures we are not preventing the future focusing of the app launcher.close() diff --git a/Modules/Launcher/Plugins/CalculatorPlugin.qml b/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml similarity index 98% rename from Modules/Launcher/Plugins/CalculatorPlugin.qml rename to Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml index e33152a17..9eca7fc7c 100644 --- a/Modules/Launcher/Plugins/CalculatorPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml @@ -1,7 +1,7 @@ import QtQuick import qs.Services import qs.Commons -import "../../../Helpers/AdvancedMath.js" as AdvancedMath +import "../../../../Helpers/AdvancedMath.js" as AdvancedMath Item { property var launcher: null diff --git a/Modules/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml similarity index 100% rename from Modules/Launcher/Plugins/ClipboardPlugin.qml rename to Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml diff --git a/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml new file mode 100644 index 000000000..113c09c84 --- /dev/null +++ b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml @@ -0,0 +1,343 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Commons +import qs.Services +import qs.Widgets +import qs.Modules.MainScreen + +// Notification History panel +SmartPanel { + id: root + + preferredWidth: 380 * Style.uiScaleRatio + preferredHeight: 480 * Style.uiScaleRatio + + onOpened: function () { + NotificationService.updateLastSeenTs() + } + + panelContent: Rectangle { + id: notificationRect + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header section + NBox { + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + (Style.marginM * 2) + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "bell" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("notifications.panel.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" + tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("tooltips.clear-history") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + NotificationService.clearHistory() + // Close panel as there is nothing more to see. + root.close() + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("tooltips.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + } + + // Empty state when no notifications + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + visible: NotificationService.historyList.count === 0 + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "bell-off" + pointSize: 64 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("notifications.panel.no-notifications") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("notifications.panel.description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + + // Notification list + // NListView { + // id: notificationList + // Layout.fillWidth: true + // Layout.fillHeight: true + // horizontalPolicy: ScrollBar.AlwaysOff + // verticalPolicy: ScrollBar.AsNeeded + + // model: NotificationService.historyList + // spacing: Style.marginM + // clip: true + // boundsBehavior: Flickable.StopAtBounds + // visible: NotificationService.historyList.count > 0 + + // // Track which notification is expanded + // property string expandedId: "" + + // delegate: Item { + // property string notificationId: model.id + // property bool isExpanded: notificationList.expandedId === notificationId + + // width: notificationList.width + // height: notificationLayoutWrapper.height + (Style.marginM * 2) + + // Behavior on height { + // enabled: !Settings.data.general.animationDisabled + // NumberAnimation { + // duration: Style.animationNormal + // easing.type: Easing.InOutQuad + // } + // } + + // Rectangle { + // anchors.fill: parent + // radius: Style.radiusM + // color: Color.mSurfaceVariant + // border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) + // border.width: Style.borderS + + // // Smooth color transition on hover + // Behavior on color { + // enabled: !Settings.data.general.animationDisabled + // ColorAnimation { + // duration: Style.animationFast + // } + // } + // } + + // // Click to expand/collapse + // MouseArea { + // anchors.fill: parent + // // Don't capture clicks on the delete button + // anchors.rightMargin: 48 + // enabled: (summaryText.truncated || bodyText.truncated) + // onClicked: { + // if (notificationList.expandedId === notificationId) { + // notificationList.expandedId = "" + // } else { + // notificationList.expandedId = notificationId + // } + // } + // cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + // } + + // Item { + // id: notificationLayoutWrapper + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.top: parent.top + // anchors.margins: Style.marginM + // height: notificationLayout.implicitHeight + + // RowLayout { + // id: notificationLayout + // width: parent.width + // spacing: Style.marginM + + // // Icon column - use simple Item instead of ColumnLayout to avoid polish loop + // Item { + // Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio) + // Layout.alignment: Qt.AlignTop + + // NImageCircled { + // anchors.top: parent.top + // anchors.topMargin: 20 + // width: Math.round(40 * Style.uiScaleRatio) + // height: Math.round(40 * Style.uiScaleRatio) + // imagePath: model.cachedImage || model.originalImage || "" + // borderColor: Color.transparent + // borderWidth: 0 + // fallbackIcon: "bell" + // fallbackIconSize: 24 + // } + // } + + // // Notification content column + // ColumnLayout { + // Layout.fillWidth: true + // Layout.alignment: Qt.AlignTop + // spacing: Style.marginXS + // Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6) + + // // Header row with app name and timestamp + // RowLayout { + // Layout.fillWidth: true + // spacing: Style.marginS + + // // Urgency indicator + // Rectangle { + // Layout.preferredWidth: 6 + // Layout.preferredHeight: 6 + // Layout.alignment: Qt.AlignVCenter + // radius: 3 + // visible: model.urgency !== 1 + // color: { + // if (model.urgency === 2) + // return Color.mError + // else if (model.urgency === 0) + // return Color.mOnSurfaceVariant + // else + // return Color.transparent + // } + // } + + // NText { + // text: model.appName || "Unknown App" + // pointSize: Style.fontSizeXS + // color: Color.mSecondary + // } + + // NText { + // text: Time.formatRelativeTime(model.timestamp) + // pointSize: Style.fontSizeXS + // color: Color.mSecondary + // } + + // Item { + // Layout.fillWidth: true + // } + // } + + // // Summary + // NText { + // id: summaryText + // text: model.summary || I18n.tr("general.no-summary") + // pointSize: Style.fontSizeM + // font.weight: Font.Medium + // color: Color.mOnSurface + // textFormat: Text.PlainText + // wrapMode: Text.Wrap + // Layout.fillWidth: true + // maximumLineCount: isExpanded ? 999 : 2 + // elide: Text.ElideRight + // } + + // // Body + // NText { + // id: bodyText + // text: model.body || "" + // pointSize: Style.fontSizeS + // color: Color.mOnSurfaceVariant + // textFormat: Text.PlainText + // wrapMode: Text.Wrap + // Layout.fillWidth: true + // maximumLineCount: isExpanded ? 999 : 3 + // elide: Text.ElideRight + // visible: text.length > 0 + // } + + // // Spacer for expand indicator + // Item { + // Layout.fillWidth: true + // Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0 + // } + + // // Expand indicator + // RowLayout { + // Layout.fillWidth: true + // visible: !isExpanded && (summaryText.truncated || bodyText.truncated) + // spacing: Style.marginXS + + // Item { + // Layout.fillWidth: true + // } + + // NText { + // text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand" + // pointSize: Style.fontSizeXS + // color: Color.mPrimary + // font.weight: Font.Medium + // } + + // NIcon { + // icon: "chevron-down" + // pointSize: Style.fontSizeS + // color: Color.mPrimary + // } + // } + // } + + // // Delete button + // NIconButton { + // icon: "trash" + // tooltipText: I18n.tr("tooltips.delete-notification") + // baseSize: Style.baseWidgetSize * 0.7 + // Layout.alignment: Qt.AlignTop + + // onClicked: { + // // Remove from history using the service API + // NotificationService.removeFromHistory(notificationId) + // } + // } + // } + // } + // } + // } + } + } +} diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/Panels/SessionMenu/SessionMenu.qml similarity index 99% rename from Modules/SessionMenu/SessionMenu.qml rename to Modules/Panels/SessionMenu/SessionMenu.qml index 877f68ef3..a241298ba 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/Panels/SessionMenu/SessionMenu.qml @@ -9,8 +9,9 @@ import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root preferredWidth: 400 * Style.uiScaleRatio @@ -148,7 +149,7 @@ NPanel { } } - // Override keyboard handlers from NPanel + // Override keyboard handlers from SmartPanel function onEscapePressed() { if (timerActive) { cancelTimer() diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml similarity index 100% rename from Modules/Settings/Bar/BarWidgetSettingsDialog.qml rename to Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml diff --git a/Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/BatterySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/BatterySettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BluetoothSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/BluetoothSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/BrightnessSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/ClockSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/ClockSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/ControlCenterSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/CustomButtonSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/MediaMiniSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/MicrophoneSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/SpacerSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/SpacerSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/TaskbarSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/TaskbarSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/TraySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/TraySettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/TraySettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/TraySettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/VolumeSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/VolumeSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/WiFiSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/WiFiSettings.qml diff --git a/Modules/Settings/Bar/WidgetSettings/WorkspaceSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml similarity index 100% rename from Modules/Settings/Bar/WidgetSettings/WorkspaceSettings.qml rename to Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml diff --git a/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml b/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml similarity index 100% rename from Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml rename to Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Panels/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml similarity index 100% rename from Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml rename to Modules/Panels/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml similarity index 99% rename from Modules/Settings/SettingsPanel.qml rename to Modules/Panels/Settings/SettingsPanel.qml index 5ddcb4b01..6db53367a 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -3,12 +3,13 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland -import qs.Modules.Settings.Tabs +import qs.Modules.Panels.Settings.Tabs import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root preferredWidth: 820 * Style.uiScaleRatio @@ -284,7 +285,7 @@ NPanel { } } - // Override keyboard handlers from NPanel + // Override keyboard handlers from SmartPanel function onTabPressed() { selectNextTab() } diff --git a/Modules/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml similarity index 100% rename from Modules/Settings/Tabs/AboutTab.qml rename to Modules/Panels/Settings/Tabs/AboutTab.qml diff --git a/Modules/Settings/Tabs/AudioTab.qml b/Modules/Panels/Settings/Tabs/AudioTab.qml similarity index 100% rename from Modules/Settings/Tabs/AudioTab.qml rename to Modules/Panels/Settings/Tabs/AudioTab.qml diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Panels/Settings/Tabs/BarTab.qml similarity index 100% rename from Modules/Settings/Tabs/BarTab.qml rename to Modules/Panels/Settings/Tabs/BarTab.qml diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml similarity index 100% rename from Modules/Settings/Tabs/ColorSchemeTab.qml rename to Modules/Panels/Settings/Tabs/ColorSchemeTab.qml diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Panels/Settings/Tabs/ControlCenterTab.qml similarity index 100% rename from Modules/Settings/Tabs/ControlCenterTab.qml rename to Modules/Panels/Settings/Tabs/ControlCenterTab.qml diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Panels/Settings/Tabs/DisplayTab.qml similarity index 100% rename from Modules/Settings/Tabs/DisplayTab.qml rename to Modules/Panels/Settings/Tabs/DisplayTab.qml diff --git a/Modules/Settings/Tabs/DockTab.qml b/Modules/Panels/Settings/Tabs/DockTab.qml similarity index 100% rename from Modules/Settings/Tabs/DockTab.qml rename to Modules/Panels/Settings/Tabs/DockTab.qml diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Panels/Settings/Tabs/GeneralTab.qml similarity index 100% rename from Modules/Settings/Tabs/GeneralTab.qml rename to Modules/Panels/Settings/Tabs/GeneralTab.qml diff --git a/Modules/Settings/Tabs/HooksTab.qml b/Modules/Panels/Settings/Tabs/HooksTab.qml similarity index 100% rename from Modules/Settings/Tabs/HooksTab.qml rename to Modules/Panels/Settings/Tabs/HooksTab.qml diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Panels/Settings/Tabs/LauncherTab.qml similarity index 100% rename from Modules/Settings/Tabs/LauncherTab.qml rename to Modules/Panels/Settings/Tabs/LauncherTab.qml diff --git a/Modules/Settings/Tabs/LocationTab.qml b/Modules/Panels/Settings/Tabs/LocationTab.qml similarity index 100% rename from Modules/Settings/Tabs/LocationTab.qml rename to Modules/Panels/Settings/Tabs/LocationTab.qml diff --git a/Modules/Settings/Tabs/LockScreenTab.qml b/Modules/Panels/Settings/Tabs/LockScreenTab.qml similarity index 100% rename from Modules/Settings/Tabs/LockScreenTab.qml rename to Modules/Panels/Settings/Tabs/LockScreenTab.qml diff --git a/Modules/Settings/Tabs/NetworkTab.qml b/Modules/Panels/Settings/Tabs/NetworkTab.qml similarity index 100% rename from Modules/Settings/Tabs/NetworkTab.qml rename to Modules/Panels/Settings/Tabs/NetworkTab.qml diff --git a/Modules/Settings/Tabs/NotificationsTab.qml b/Modules/Panels/Settings/Tabs/NotificationsTab.qml similarity index 100% rename from Modules/Settings/Tabs/NotificationsTab.qml rename to Modules/Panels/Settings/Tabs/NotificationsTab.qml diff --git a/Modules/Settings/Tabs/OsdTab.qml b/Modules/Panels/Settings/Tabs/OsdTab.qml similarity index 100% rename from Modules/Settings/Tabs/OsdTab.qml rename to Modules/Panels/Settings/Tabs/OsdTab.qml diff --git a/Modules/Settings/Tabs/ScreenRecorderTab.qml b/Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml similarity index 100% rename from Modules/Settings/Tabs/ScreenRecorderTab.qml rename to Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml similarity index 100% rename from Modules/Settings/Tabs/UserInterfaceTab.qml rename to Modules/Panels/Settings/Tabs/UserInterfaceTab.qml diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Panels/Settings/Tabs/WallpaperTab.qml similarity index 100% rename from Modules/Settings/Tabs/WallpaperTab.qml rename to Modules/Panels/Settings/Tabs/WallpaperTab.qml diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/Panels/SetupWizard/SetupAppearanceStep.qml similarity index 100% rename from Modules/SetupWizard/SetupAppearanceStep.qml rename to Modules/Panels/SetupWizard/SetupAppearanceStep.qml diff --git a/Modules/SetupWizard/SetupCustomizeStep.qml b/Modules/Panels/SetupWizard/SetupCustomizeStep.qml similarity index 100% rename from Modules/SetupWizard/SetupCustomizeStep.qml rename to Modules/Panels/SetupWizard/SetupCustomizeStep.qml diff --git a/Modules/SetupWizard/SetupDockStep.qml b/Modules/Panels/SetupWizard/SetupDockStep.qml similarity index 100% rename from Modules/SetupWizard/SetupDockStep.qml rename to Modules/Panels/SetupWizard/SetupDockStep.qml diff --git a/Modules/SetupWizard/SetupWallpaperStep.qml b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml similarity index 99% rename from Modules/SetupWizard/SetupWallpaperStep.qml rename to Modules/Panels/SetupWizard/SetupWallpaperStep.qml index 4725247d6..d1d0df06d 100644 --- a/Modules/SetupWizard/SetupWallpaperStep.qml +++ b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml @@ -7,7 +7,7 @@ import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets -import "../../Helpers/FuzzySort.js" as FuzzySort +import "../../../Helpers/FuzzySort.js" as FuzzySort ColumnLayout { id: root diff --git a/Modules/SetupWizard/SetupWizard.qml b/Modules/Panels/SetupWizard/SetupWizard.qml similarity index 99% rename from Modules/SetupWizard/SetupWizard.qml rename to Modules/Panels/SetupWizard/SetupWizard.qml index 996d7d577..d3cccadee 100644 --- a/Modules/SetupWizard/SetupWizard.qml +++ b/Modules/Panels/SetupWizard/SetupWizard.qml @@ -6,8 +6,9 @@ import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root preferredWidth: 520 * Style.uiScaleRatio diff --git a/Modules/Bar/Extras/TrayDropdownPanel.qml b/Modules/Panels/Tray/TrayDropdownPanel.qml similarity index 97% rename from Modules/Bar/Extras/TrayDropdownPanel.qml rename to Modules/Panels/Tray/TrayDropdownPanel.qml index cd41b98c6..5e3517cdb 100644 --- a/Modules/Bar/Extras/TrayDropdownPanel.qml +++ b/Modules/Panels/Tray/TrayDropdownPanel.qml @@ -6,13 +6,12 @@ import Quickshell.Services.SystemTray import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen // A compact grid panel listing all tray items, opened from the Tray widget -NPanel { +SmartPanel { id: root - objectName: "trayDropdownPanel" - // Widget info for menu functionality property string widgetSection: "" property int widgetIndex: -1 @@ -79,7 +78,7 @@ NPanel { preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding) preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding) - // Positioning is handled automatically by NPanel when toggle(buttonItem) is called + // Positioning is handled automatically by SmartPanel when toggle(buttonItem) is called // Watch for settings changes to refresh the dropdown Connections { @@ -177,7 +176,5 @@ NPanel { } } } - - // (Tray menu now uses dedicated TrayMenu via PanelService) } } diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Panels/Tray/TrayMenuPanel.qml similarity index 98% rename from Modules/Bar/Extras/TrayMenu.qml rename to Modules/Panels/Tray/TrayMenuPanel.qml index 32e551754..94fa5f386 100644 --- a/Modules/Bar/Extras/TrayMenu.qml +++ b/Modules/Panels/Tray/TrayMenuPanel.qml @@ -5,17 +5,11 @@ import Quickshell import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root - objectName: "trayMenu" - - // Avoid bouncy feel: keep fade but disable scale/slide transforms - disableScaleAnimation: true - disableSlideAnimation: false - customSlideDistance: 100 - // Inputs property QsMenuHandle menu property var trayItem: null @@ -41,7 +35,7 @@ NPanel { // Only show submenu in place (replace main menu) property bool inPlaceSubmenu: true - // Let NPanel size to our content + // Let Panel size to our content readonly property real contentPreferredWidth: root.menuWidth readonly property real contentPreferredHeight: { // If showing submenu in-place, size to the submenu's flickable content height diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Panels/Wallpaper/WallpaperPanel.qml similarity index 99% rename from Modules/Wallpaper/WallpaperPanel.qml rename to Modules/Panels/Wallpaper/WallpaperPanel.qml index 5a7e1bbf1..eb6935138 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -4,12 +4,13 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Modules.Settings +import qs.Modules.Panels.Settings import qs.Services import qs.Widgets -import "../../Helpers/FuzzySort.js" as FuzzySort +import qs.Modules.MainScreen +import "../../../Helpers/FuzzySort.js" as FuzzySort -NPanel { +SmartPanel { id: root preferredWidth: 800 * Style.uiScaleRatio diff --git a/Modules/Bar/WiFi/WiFiPanel.qml b/Modules/Panels/WiFi/WiFiPanel.qml similarity index 99% rename from Modules/Bar/WiFi/WiFiPanel.qml rename to Modules/Panels/WiFi/WiFiPanel.qml index 29b096858..2bb8ffb54 100644 --- a/Modules/Bar/WiFi/WiFiPanel.qml +++ b/Modules/Panels/WiFi/WiFiPanel.qml @@ -6,8 +6,9 @@ import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets +import qs.Modules.MainScreen -NPanel { +SmartPanel { id: root preferredWidth: 420 * Style.uiScaleRatio diff --git a/Modules/Settings/DirectWidgetSettingsPanel.qml b/Modules/Settings/DirectWidgetSettingsPanel.qml deleted file mode 100644 index cd2cde58a..000000000 --- a/Modules/Settings/DirectWidgetSettingsPanel.qml +++ /dev/null @@ -1,70 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.Modules.Settings.Tabs -import qs.Commons -import qs.Services -import qs.Widgets - -NPanel { - id: root - - preferredWidth: 0 - preferredHeight: 0 - panelBackgroundColor: Color.transparent - panelBorderColor: Color.transparent - - property var requestedWidgetSettings: [] - - panelContent: Item { - - Component.onCompleted: { - Qt.callLater(() => { - var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/Bar/BarWidgetSettingsDialog.qml")) - - function instantiateAndOpen() { - var dialog = component.createObject(Overlay.overlay, { - "widgetIndex": requestedWidgetSettings.widgetIndex, - "widgetData": requestedWidgetSettings.widgetData, - "widgetId": requestedWidgetSettings.widgetId, - "sectionId": requestedWidgetSettings.sectionId - }) - if (dialog) { - dialog.updateWidgetSettings.connect(updateWidgetSettingsInSection) - dialog.onClosed.connect(closeWidgetSettings) - dialog.open() - } - } - - if (component.status === Component.Ready) { - instantiateAndOpen() - } else { - component.statusChanged.connect(instantiateAndOpen) - } - - // Clear the request after handling - requestedWidgetSettings = [] - }) - } - } - - function openWidgetSettings(section, widgetIndex, widgetId, widgetData) { - requestedWidgetSettings = { - "sectionId": section, - "widgetIndex": widgetIndex, - "widgetId": widgetId, - "widgetData": widgetData - } - root.open() - } - - function updateWidgetSettingsInSection(section, index, settings) { - Settings.data.bar.widgets[section][index] = settings - } - - function closeWidgetSettings() { - root.close() - } -} diff --git a/Services/CavaService.qml b/Services/CavaService.qml index cf5c174cf..16324bc76 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -15,7 +15,7 @@ Singleton { * - LockScreen is opened * - A control center is open */ - property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) property var values: Array(barsCount).fill(0) property int barsCount: 48 diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml index 5a1431f13..b5da5473e 100644 --- a/Services/ControlCenterWidgetRegistry.qml +++ b/Services/ControlCenterWidgetRegistry.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import qs.Commons -import qs.Modules.ControlCenter.Widgets +import qs.Modules.Panels.ControlCenter.Widgets Singleton { id: root diff --git a/Modules/AudioSpectrum/LinearSpectrum.qml b/Widgets/AudioSpectrum/LinearSpectrum.qml similarity index 100% rename from Modules/AudioSpectrum/LinearSpectrum.qml rename to Widgets/AudioSpectrum/LinearSpectrum.qml diff --git a/Modules/AudioSpectrum/MirroredSpectrum.qml b/Widgets/AudioSpectrum/MirroredSpectrum.qml similarity index 100% rename from Modules/AudioSpectrum/MirroredSpectrum.qml rename to Widgets/AudioSpectrum/MirroredSpectrum.qml diff --git a/Modules/AudioSpectrum/WaveSpectrum.qml b/Widgets/AudioSpectrum/WaveSpectrum.qml similarity index 100% rename from Modules/AudioSpectrum/WaveSpectrum.qml rename to Widgets/AudioSpectrum/WaveSpectrum.qml diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml deleted file mode 100644 index f1d08fd55..000000000 --- a/Widgets/NFullScreenWindow.qml +++ /dev/null @@ -1,616 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services - - -/** - * NFullScreenWindow - Single PanelWindow per screen that manages all panels and the bar - */ -PanelWindow { - id: root - - required property var barComponent - required property var panelComponents - - Component.onCompleted: { - Logger.d("NFullScreenWindow", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) - } - - // Debug: Log mask region changes - onMaskChanged: { - Logger.d("NFullScreenWindow", "Mask changed!") - Logger.d("NFullScreenWindow", " Bar region:", barLoader.item?.barRegion) - Logger.d("NFullScreenWindow", " Panel count:", panelsRepeater.count) - for (var i = 0; i < panelsRepeater.count; i++) { - var panelItem = panelsRepeater.itemAt(i)?.item - Logger.d("NFullScreenWindow", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion) - } - } - - // Wayland - // Always use Exclusive keyboard focus when a panel is open - // This ensures all keyboard shortcuts work reliably (Escape, etc.) - // The centralized shortcuts in this window handle delegation to panels - WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - WlrLayershell.layer: Settings.data.ui.panelsOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top - WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") - WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that - - anchors { - top: true - bottom: true - left: true - right: true - } - - // Desktop dimming when panels are open - property bool dimDesktop: Settings.data.general.dimDesktop - property bool isPanelOpen: PanelService.openedPanel !== null - color: { - if (dimDesktop && isPanelOpen) { - return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy) - } - return Color.transparent - } - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - function updateMask() { - // Build the regions list - var regionsList = [barMaskRegion] - - // Add background region if a panel is open - // This makes the background clickable (not click-through) so we can detect clicks to close panels - if (root.isPanelOpen) { - regionsList.push(backgroundMaskRegion) - } - - // Add regions for each open panel - // Only include panels that are open AND not closing (to allow click-through during close animation) - for (var i = 0; i < panelMaskRepeater.count; i++) { - var wrapperItem = panelMaskRepeater.itemAt(i) - if (wrapperItem && wrapperItem.maskRegion) { - var panelItem = wrapperItem.panelItem - if (panelItem && panelItem.isPanelOpen && !panelItem.isClosing) { - var panelRegion = panelItem.panelRegion - // Update the mask region's coordinates from the panel's actual region - if (panelRegion) { - wrapperItem.maskRegion.x = panelRegion.x - wrapperItem.maskRegion.y = panelRegion.y - wrapperItem.maskRegion.width = panelRegion.width - wrapperItem.maskRegion.height = panelRegion.height - regionsList.push(wrapperItem.maskRegion) - } - } - } - } - - // Update the mask's regions - clickableMask.regions = regionsList - } - - // Listen to PanelService to update mask when panels open/close - Connections { - target: PanelService - function onWillOpen() { - root.updateMask() - } - function onDidClose() { - // Delay mask update to ensure panel's isPanelOpen is updated first - Qt.callLater(() => root.updateMask()) - } - } - - // Background region - for closing panels when clicking outside (separate from mask) - Region { - id: backgroundMaskRegion - x: 0 - y: 0 - width: root.width - height: root.height - intersection: Intersection.Subtract - } - - // Smart mask: Make everything click-through except bar and open panels - mask: Region { - id: clickableMask - - // Cover entire window (everything is masked/click-through) - x: 0 - y: 0 - width: root.width - height: root.height - intersection: Intersection.Xor - - // Regions list is set programmatically in updateMask() - // Initially just the bar - regions: [barMaskRegion] - - // Bar region - subtract bar area from mask - Region { - id: barMaskRegion - property var barRegion: barLoader.item && barLoader.item.barRegion ? barLoader.item.barRegion : null - - x: barRegion ? barRegion.x : 0 - y: barRegion ? barRegion.y : 0 - width: barRegion ? barRegion.width : 0 - height: barRegion ? barRegion.height : 0 - intersection: Intersection.Subtract - - // Update mask when bar geometry changes - onWidthChanged: Qt.callLater(() => root.updateMask()) - onHeightChanged: Qt.callLater(() => root.updateMask()) - } - } - - // Container for panel mask regions (created dynamically) - Item { - id: panelMaskRegions - - // Create a Region for each panel - Repeater { - id: panelMaskRepeater - model: panelsRepeater.count - - delegate: Item { - required property int index - property var panelItem: panelsRepeater.itemAt(index)?.item - property var region: panelItem && panelItem.panelRegion ? panelItem.panelRegion : null - - // The actual mask region as a child - property alias maskRegion: panelMask - - Region { - id: panelMask - // Coordinates are set programmatically in updateMask() - intersection: Intersection.Subtract - } - } - } - } - - // Container for all UI elements - Item { - id: container - width: root.width - height: root.height - - // Apply shadow effect - layer.enabled: Settings.data.general.enableShadows - layer.smooth: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowOpacity: Style.shadowOpacity - shadowHorizontalOffset: Style.shadowHorizontalOffset - shadowVerticalOffset: Style.shadowVerticalOffset - shadowColor: Color.black - blur: Style.shadowBlur - blurMax: Style.shadowBlurMax - } - - // Screen corners (integrated to avoid separate PanelWindow) - // Always positioned at actual screen edges - Loader { - id: screenCornersLoader - active: Settings.data.general.showScreenCorners - - anchors.fill: parent - z: 1000 // Very high z-index to be on top of everything - - sourceComponent: Item { - id: cornersRoot - anchors.fill: parent - - property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Color.black : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) - property real cornerRadius: Style.screenRadius - property real cornerSize: Style.screenRadius - - // Top-left concave corner - Canvas { - id: topLeftCorner - anchors.top: parent.top - anchors.left: parent.left - width: cornersRoot.cornerSize - height: cornersRoot.cornerSize - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true - - onPaint: { - const ctx = getContext("2d") - if (!ctx) - return - - ctx.reset() - ctx.clearRect(0, 0, width, height) - ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) - ctx.fillRect(0, 0, width, height) - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = Color.white - ctx.beginPath() - ctx.arc(width, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) - ctx.fill() - } - - onWidthChanged: if (available) - requestPaint() - onHeightChanged: if (available) - requestPaint() - } - - // Top-right concave corner - Canvas { - id: topRightCorner - anchors.top: parent.top - anchors.right: parent.right - width: cornersRoot.cornerSize - height: cornersRoot.cornerSize - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true - - onPaint: { - const ctx = getContext("2d") - if (!ctx) - return - - ctx.reset() - ctx.clearRect(0, 0, width, height) - ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) - ctx.fillRect(0, 0, width, height) - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = Color.white - ctx.beginPath() - ctx.arc(0, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) - ctx.fill() - } - - onWidthChanged: if (available) - requestPaint() - onHeightChanged: if (available) - requestPaint() - } - - // Bottom-left concave corner - Canvas { - id: bottomLeftCorner - anchors.bottom: parent.bottom - anchors.left: parent.left - width: cornersRoot.cornerSize - height: cornersRoot.cornerSize - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true - - onPaint: { - const ctx = getContext("2d") - if (!ctx) - return - - ctx.reset() - ctx.clearRect(0, 0, width, height) - ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) - ctx.fillRect(0, 0, width, height) - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = Color.white - ctx.beginPath() - ctx.arc(width, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) - ctx.fill() - } - - onWidthChanged: if (available) - requestPaint() - onHeightChanged: if (available) - requestPaint() - } - - // Bottom-right concave corner - Canvas { - id: bottomRightCorner - anchors.bottom: parent.bottom - anchors.right: parent.right - width: cornersRoot.cornerSize - height: cornersRoot.cornerSize - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true - - onPaint: { - const ctx = getContext("2d") - if (!ctx) - return - - ctx.reset() - ctx.clearRect(0, 0, width, height) - ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) - ctx.fillRect(0, 0, width, height) - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = Color.white - ctx.beginPath() - ctx.arc(0, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) - ctx.fill() - } - - onWidthChanged: if (available) - requestPaint() - onHeightChanged: if (available) - requestPaint() - } - - // Repaint all corners when color or radius changes - onCornerColorChanged: { - if (topLeftCorner.available) - topLeftCorner.requestPaint() - if (topRightCorner.available) - topRightCorner.requestPaint() - if (bottomLeftCorner.available) - bottomLeftCorner.requestPaint() - if (bottomRightCorner.available) - bottomRightCorner.requestPaint() - } - - onCornerRadiusChanged: { - if (topLeftCorner.available) - topLeftCorner.requestPaint() - if (topRightCorner.available) - topRightCorner.requestPaint() - if (bottomLeftCorner.available) - bottomLeftCorner.requestPaint() - if (bottomRightCorner.available) - bottomRightCorner.requestPaint() - } - } - } - - // Background MouseArea for closing panels when clicking outside - // Active whenever a panel is open - the mask ensures it only receives clicks when panel is open - MouseArea { - anchors.fill: parent - enabled: root.isPanelOpen - onClicked: { - if (PanelService.openedPanel) { - PanelService.openedPanel.close() - } - } - z: 0 // Behind panels and bar - } - - // All panels (as Items, not PanelWindows) - Repeater { - id: panelsRepeater - model: root.panelComponents - - delegate: Loader { - id: panelLoader - - // Lazy load panels - only create when first requested - // Panel stays loaded once created for faster subsequent opens - active: false - asynchronous: false - sourceComponent: modelData.component - - // Fill the container so panels have proper parent dimensions - anchors.fill: parent - - // Panel properties binding - property var panelScreen: root.screen - property string panelId: modelData.id - property int panelZIndex: modelData.zIndex || 50 - property bool hasBeenRequested: false - - Component.onCompleted: { - // Register the loader immediately so PanelService can load it on-demand - var objectName = panelId + "-" + (panelScreen?.name || "unknown") - PanelService.registerPanelLoader(panelLoader, objectName) - } - - // Activate loader when panel is first requested - function ensureLoaded() { - if (!hasBeenRequested) { - Logger.d("NFullScreenWindow", "Loading panel on-demand:", panelId) - hasBeenRequested = true - active = true - } - } - - onLoaded: { - if (item) { - // Set unique objectName per screen BEFORE registration: "calendarPanel-DP-1" - item.objectName = panelId + "-" + (panelScreen?.name || "unknown") - item.screen = panelScreen - PanelService.registerPanel(item) - Logger.d("NFullScreenWindow", "Panel loaded with objectName:", item.objectName, "on screen:", panelScreen?.name) - } - } - } - } - - // Bar (always on top) - Loader { - id: barLoader - asynchronous: false - sourceComponent: root.barComponent - // Keep bar loaded but hide it when BarService.isVisible is false - // This allows panels to remain accessible via IPC - visible: BarService.isVisible - - // Fill parent to provide dimensions for Bar to reference - anchors.fill: parent - - property ShellScreen screen: root.screen - - onLoaded: { - Logger.d("NFullScreenWindow", "Bar loaded:", item !== null) - if (item) { - Logger.d("NFullScreenWindow", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height) - // Bind screen to bar component (use binding for reactivity) - item.screen = Qt.binding(function () { - return barLoader.screen - }) - } - } - } - } - - // Centralized keyboard shortcuts - delegate to opened panel - // This ensures shortcuts work regardless of panel focus state - Shortcut { - sequence: "Escape" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) { - PanelService.openedPanel.onEscapePressed() - } else if (PanelService.openedPanel) { - PanelService.openedPanel.close() - } - } - } - - Shortcut { - sequence: "Tab" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) { - PanelService.openedPanel.onTabPressed() - } - } - } - - Shortcut { - sequence: "Shift+Tab" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) { - PanelService.openedPanel.onShiftTabPressed() - } - } - } - - Shortcut { - sequence: "Up" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) { - PanelService.openedPanel.onUpPressed() - } - } - } - - Shortcut { - sequence: "Down" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) { - PanelService.openedPanel.onDownPressed() - } - } - } - - Shortcut { - sequence: "Return" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { - PanelService.openedPanel.onReturnPressed() - } - } - } - - Shortcut { - sequence: "Enter" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { - PanelService.openedPanel.onReturnPressed() - } - } - } - - Shortcut { - sequence: "Home" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) { - PanelService.openedPanel.onHomePressed() - } - } - } - - Shortcut { - sequence: "End" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) { - PanelService.openedPanel.onEndPressed() - } - } - } - - Shortcut { - sequence: "PgUp" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) { - PanelService.openedPanel.onPageUpPressed() - } - } - } - - Shortcut { - sequence: "PgDown" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) { - PanelService.openedPanel.onPageDownPressed() - } - } - } - - Shortcut { - sequence: "Ctrl+J" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) { - PanelService.openedPanel.onCtrlJPressed() - } - } - } - - Shortcut { - sequence: "Ctrl+K" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) { - PanelService.openedPanel.onCtrlKPressed() - } - } - } - - Shortcut { - sequence: "Left" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) { - PanelService.openedPanel.onLeftPressed() - } - } - } - - Shortcut { - sequence: "Right" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) { - PanelService.openedPanel.onRightPressed() - } - } - } -} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml deleted file mode 100644 index 4bdf0e504..000000000 --- a/Widgets/NPanel.qml +++ /dev/null @@ -1,858 +0,0 @@ -import QtQuick -import Quickshell -import qs.Commons -import qs.Services - - -/** - * NPanel for use within NFullScreenWindow - */ -Item { - id: root - - // Screen property provided by NFullScreenWindow - property ShellScreen screen: null - - // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap - property real edgeSnapDistance: 50 - - property Component panelContent: null - - // Panel size properties - property real preferredWidth: 700 - property real preferredHeight: 900 - property real preferredWidthRatio - property real preferredHeightRatio - property color panelBackgroundColor: Color.mSurface - property color panelBorderColor: Color.mOutline - property var buttonItem: null - - // Anchoring properties - property bool panelAnchorHorizontalCenter: false - property bool panelAnchorVerticalCenter: false - property bool panelAnchorTop: false - property bool panelAnchorBottom: false - property bool panelAnchorLeft: false - property bool panelAnchorRight: false - - // Button position properties - property bool useButtonPosition: false - property point buttonPosition: Qt.point(0, 0) - property int buttonWidth: 0 - property int buttonHeight: 0 - - // Track whether panel is open - property bool isPanelOpen: false - - // Animation properties - property real animationProgress: 0 - property bool isClosing: false - // Per-panel animation overrides - property bool disableScaleAnimation: false - property bool disableSlideAnimation: false - // If >= 0, use this pixel distance for slide instead of default - property int customSlideDistance: -1 - - // Keyboard event handlers - override these in specific panels to handle shortcuts - // These are called from NFullScreenWindow's centralized shortcuts - function onEscapePressed() { - close() - } - function onTabPressed() {} - function onShiftTabPressed() {} - function onUpPressed() {} - function onDownPressed() {} - function onLeftPressed() {} - function onRightPressed() {} - function onReturnPressed() {} - function onHomePressed() {} - function onEndPressed() {} - function onPageUpPressed() {} - function onPageDownPressed() {} - function onCtrlJPressed() {} - function onCtrlKPressed() {} - - Behavior on animationProgress { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuint - onRunningChanged: { - // When close animation finishes, actually hide the panel - if (!running && root.isClosing) { - root.isClosing = false - root.isPanelOpen = false - } - } - } - } - - // Expose panel region for click-through mask (only when open) - readonly property var panelRegion: panelContentContainer.item?.maskRegion || null - - readonly property string barPosition: Settings.data.bar.position - readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" - readonly property bool barFloating: Settings.data.bar.floating || false - readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - - // Helper to detect if any anchor is explicitly set - readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight - readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom - - signal opened - signal closed - - // Panel visibility and sizing - // Keep visible during close animation - visible: isPanelOpen || isClosing - width: parent ? parent.width : 0 - height: parent ? parent.height : 0 - - // Panel control functions - function toggle(buttonItem, buttonName) { - if (!isPanelOpen) { - open(buttonItem, buttonName) - } else { - close() - } - } - - function open(buttonItem, buttonName) { - if (!buttonItem && buttonName) { - buttonItem = BarService.lookupWidget(buttonName, screen.name) - } - - if (buttonItem) { - root.buttonItem = buttonItem - // Map button position to screen coordinates - var buttonPos = buttonItem.mapToItem(null, 0, 0) - root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y) - root.buttonWidth = buttonItem.width - root.buttonHeight = buttonItem.height - root.useButtonPosition = true - } else { - // No button provided: reset button position mode - root.buttonItem = null - root.useButtonPosition = false - } - - setPosition() - isPanelOpen = true - animationProgress = 1 - - // Notify PanelService - PanelService.willOpenPanel(root) - - // Delay the opened signal to ensure content is fully loaded - // This ensures Component.onCompleted of the loaded content runs first - Qt.callLater(() => { - opened() - }) - - Logger.d("NPanel", "Opened panel", objectName) - Logger.d("NPanel", " Root size:", width, "x", height) - } - - function close() { - // Start close animation - isClosing = true - animationProgress = 0 - - // Notify PanelService immediately - PanelService.closedPanel(root) - - // Emit closed signal - closed() - - Logger.d("NPanel", "Closing panel with animation", objectName) - // isPanelOpen will be set to false when animation completes - } - - function setPosition() {// Position calculation will be handled here - // For now, panels will be positioned based on anchors - } - - // Loader for panel content - Loader { - id: panelContentContainer - anchors.fill: parent - // Keep active during close animation - active: root.isPanelOpen || root.isClosing - asynchronous: false - - sourceComponent: Item { - anchors.fill: parent - - // Screen-dependent attachment properties (moved from root to avoid race condition) - // By the time this Loader is active, screen has been assigned by NFullScreenWindow - readonly property bool couldAttach: Settings.data.ui.panelsAttachedToBar - readonly property bool couldAttachToBar: { - if (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity < 1.0) { - return false - } - - // A panel can only be attached to a bar if there is a bar on that screen - var monitors = Settings.data.bar.monitors || [] - var result = monitors.length === 0 || monitors.includes(root.screen?.name || "") - return result - } - - // Effective anchor properties (moved from root, depend on couldAttach) - // These are true when: - // 1. Explicitly anchored, OR - // 2. Using button position and bar is on that edge, OR - // 3. Attached to bar with no explicit anchors (default centering behavior) - readonly property bool effectivePanelAnchorTop: root.panelAnchorTop || (root.useButtonPosition && root.barPosition === "top") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "top" && !root.barIsVertical) - readonly property bool effectivePanelAnchorBottom: root.panelAnchorBottom || (root.useButtonPosition && root.barPosition === "bottom") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "bottom" && !root.barIsVertical) - readonly property bool effectivePanelAnchorLeft: root.panelAnchorLeft || (root.useButtonPosition && root.barPosition === "left") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "left" && root.barIsVertical) - readonly property bool effectivePanelAnchorRight: root.panelAnchorRight || (root.useButtonPosition && root.barPosition === "right") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "right" && root.barIsVertical) - - // Expose panelBackground for mask region - property alias maskRegion: panelBackground - - // The actual panel background and content - Item { - anchors.fill: parent - - NShapedRectangle { - id: panelBackground - - backgroundColor: root.panelBackgroundColor - - Behavior on backgroundColor { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - Behavior on width { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - Behavior on height { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - // Check if panel has any inverted corners - readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted - - // Determine panel attachment type for animation - readonly property bool isAttachedToFloatingBar: hasInvertedCorners && root.barFloating && couldAttach - readonly property bool isAttachedToNonFloating: hasInvertedCorners && (!root.barFloating || !couldAttach) - readonly property bool isDetached: !hasInvertedCorners - - // Determine closest screen edge to slide from (for full slide animation) - readonly property string slideDirection: { - if (!isAttachedToNonFloating) - return "none" - - // Priority: If panel is touching the bar (but not touching any screen edge), slide from the bar direction - // This handles cases where centered panels snap to the bar due to height constraints - // If touching screen edges, fall through to the distance-based calculation below - // var touchingAnyScreenEdge = touchingLeftEdge || touchingRightEdge || touchingTopEdge || touchingBottomEdge - // if (!touchingAnyScreenEdge) { - if (touchingTopBar && root.barPosition === "top") - return "top" - if (touchingBottomBar && root.barPosition === "bottom") - return "bottom" - if (touchingLeftBar && root.barPosition === "left") - return "left" - if (touchingRightBar && root.barPosition === "right") - return "right" - //} - - // Use panel's center point (barycenter) as reference - var centerX = x + width / 2 - var centerY = y + height / 2 - - // Calculate actual travel distances (barycenter to screen edge) - var travelFromTop = centerY - var travelFromBottom = parent.height - centerY - var travelFromLeft = centerX - var travelFromRight = parent.width - centerX - - // Find minimum travel distance - var minTravel = Math.min(travelFromTop, travelFromBottom, travelFromLeft, travelFromRight) - - // Return the direction with least travel distance - if (minTravel === travelFromTop) - return "top" - if (minTravel === travelFromBottom) - return "bottom" - if (minTravel === travelFromLeft) - return "left" - if (minTravel === travelFromRight) - return "right" - return "none" - } - - // Animation offset calculation - readonly property real slideOffset: { - if (root.disableSlideAnimation) - return 0 - if (root.customSlideDistance >= 0) { - return (1 - root.animationProgress) * root.customSlideDistance - } - // Full slide for non-floating attached panels - if (isAttachedToNonFloating) { - var distance = (slideDirection === "left" || slideDirection === "right") ? width : height - return Math.round((1 - root.animationProgress) * distance) - } - // Small 40px slide for floating bar attached panels - if (isAttachedToFloatingBar) { - return (1 - root.animationProgress) * 40 - } - // No slide for detached panels - return 0 - } - - // Animation properties - opacity: isAttachedToNonFloating ? Math.min(1, root.animationProgress * 5) : root.animationProgress - scale: { - if (root.disableScaleAnimation) - return 1 - if (isAttachedToNonFloating) - return 1 // No scale for full slide animation - if (isAttachedToFloatingBar) - return 1 // No scale for floating bar (40px slide + opacity only) - return (0.9 + root.animationProgress * 0.1) // Scale for detached panels - } - - // Transform origin for scale animation - transformOrigin: Item.Center - - // Slide animation using transform - transform: Translate { - x: { - // Full slide from nearest edge for non-floating attached panels - if (panelBackground.isAttachedToNonFloating) { - if (panelBackground.slideDirection === "left") - return -panelBackground.slideOffset - if (panelBackground.slideDirection === "right") - return panelBackground.slideOffset - return 0 - } - // Small 40px slide from bar for floating bar attached panels - if (panelBackground.isAttachedToFloatingBar) { - if (root.barPosition === "left") - return -panelBackground.slideOffset - if (root.barPosition === "right") - return panelBackground.slideOffset - } - return 0 - } - y: { - // Full slide from nearest edge for non-floating attached panels - if (panelBackground.isAttachedToNonFloating) { - if (panelBackground.slideDirection === "top") - return -panelBackground.slideOffset - if (panelBackground.slideDirection === "bottom") - return panelBackground.slideOffset - return 0 - } - // Small 40px slide from bar for floating bar attached panels - if (panelBackground.isAttachedToFloatingBar) { - if (root.barPosition === "top") - return -panelBackground.slideOffset - if (root.barPosition === "bottom") - return panelBackground.slideOffset - } - return 0 - } - } - - topLeftRadius: Style.radiusL - topRightRadius: Style.radiusL - bottomLeftRadius: Style.radiusL - bottomRightRadius: Style.radiusL - - // Inverted corners based on bar attachment - // When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted - // Also invert corners when touching screen edges (non-floating bar only) - topLeftInverted: { - // Bar attachment: only attach to bar if bar opacity >= 1.0 (no color clash) - var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft)) - // Also detect when panel touches bar edge (e.g., centered panel that's too tall) - var barTouchInverted = touchingTopBar || touchingLeftBar - // Screen edge contact: can attach to screen edges even if bar opacity < 1.0 - // For horizontal bars: invert when touching left/right edges - // For vertical bars: invert when touching top/bottom edges - var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical)) - // Also invert when touching screen edge opposite to bar (e.g., bottom edge when bar is at top) - var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top") - return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted - } - topRightInverted: { - var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight)) - var barTouchInverted = touchingTopBar || touchingRightBar - var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical)) - var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top") - return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted - } - bottomLeftInverted: { - var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft)) - var barTouchInverted = touchingBottomBar || touchingLeftBar - var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical)) - var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom") - return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted - } - bottomRightInverted: { - var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight)) - var barTouchInverted = touchingBottomBar || touchingRightBar - var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical)) - var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom") - return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted - } - - // Set inverted corner direction based on which edge touches - // Bar edges: horizontal bars → horizontal curves, vertical bars → vertical curves - // Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves - topLeftInvertedDirection: { - if (touchingLeftEdge && !root.barIsVertical) - return "vertical" - if (touchingTopEdge && root.barIsVertical) - return "horizontal" - return root.barIsVertical ? "vertical" : "horizontal" - } - topRightInvertedDirection: { - if (touchingRightEdge && !root.barIsVertical) - return "vertical" - if (touchingTopEdge && root.barIsVertical) - return "horizontal" - return root.barIsVertical ? "vertical" : "horizontal" - } - bottomLeftInvertedDirection: { - if (touchingLeftEdge && !root.barIsVertical) - return "vertical" - if (touchingBottomEdge && root.barIsVertical) - return "horizontal" - return root.barIsVertical ? "vertical" : "horizontal" - } - bottomRightInvertedDirection: { - if (touchingRightEdge && !root.barIsVertical) - return "vertical" - if (touchingBottomEdge && root.barIsVertical) - return "horizontal" - return root.barIsVertical ? "vertical" : "horizontal" - } - width: { - var w - // Priority 1: Content-driven size (dynamic) - if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) { - w = contentLoader.item.contentPreferredWidth - } // Priority 2: Ratio-based size - else if (root.preferredWidthRatio !== undefined) { - w = Math.round(Math.max((parent.width || 1920) * root.preferredWidthRatio, root.preferredWidth)) - } // Priority 3: Static preferred width - else { - w = root.preferredWidth - } - return Math.min(w, (parent.width || 1920) - Style.marginL * 2) - } - - height: { - var h - // Priority 1: Content-driven size (dynamic) - if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) { - h = contentLoader.item.contentPreferredHeight - } // Priority 2: Ratio-based size - else if (root.preferredHeightRatio !== undefined) { - h = Math.round(Math.max((parent.height || 1080) * root.preferredHeightRatio, root.preferredHeight)) - } // Priority 3: Static preferred height - else { - h = root.preferredHeight - } - return Math.min(h, (parent.height || 1080) - Style.barHeight - Style.marginL * 2) - } - - // Detect if panel is touching screen edges - readonly property bool touchingLeftEdge: couldAttach && x <= 1 - readonly property bool touchingRightEdge: couldAttach && (x + width) >= (parent.width - 1) - readonly property bool touchingTopEdge: couldAttach && y <= 1 - readonly property bool touchingBottomEdge: couldAttach && (y + height) >= (parent.height - 1) - - // Detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints) - readonly property bool touchingTopBar: couldAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(y - (root.barMarginV + Style.barHeight)) <= 1 - readonly property bool touchingBottomBar: couldAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((y + height) - (parent.height - root.barMarginV - Style.barHeight)) <= 1 - readonly property bool touchingLeftBar: couldAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(x - (root.barMarginH + Style.barHeight)) <= 1 - readonly property bool touchingRightBar: couldAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((x + width) - (parent.width - root.barMarginH - Style.barHeight)) <= 1 - - // Position the panel using explicit x/y coordinates (no anchors) - // This makes coordinates clearer for the click-through mask system - x: { - var calculatedX - - // If useButtonPosition is enabled, align panel X with button - // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid - // after the source panel (e.g., ControlCenter) closes, but we still have valid position data - if (root.useButtonPosition && parent.width > 0 && width > 0) { - if (root.barIsVertical) { - // For vertical bars - if (couldAttach) { - // Attached panels: align with bar edge (left or right side) - if (root.barPosition === "left") { - // Panel to the right of left bar - var leftBarEdge = root.barMarginH + Style.barHeight - // Panel sits right at bar edge (inverted corners align perfectly) - calculatedX = leftBarEdge - } else { - // right - // Panel to the left of right bar - var rightBarEdge = parent.width - root.barMarginH - Style.barHeight - // Panel sits right at bar edge (inverted corners align perfectly) - calculatedX = rightBarEdge - width - } - } else { - // Detached panels: center on button X position - var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 - // Clamp to screen bounds with margins, accounting for bar position - var minX = Style.marginL - var maxX = parent.width - width - Style.marginL - - // Account for vertical bar taking up space - if (root.barPosition === "left") { - minX = root.barMarginH + Style.barHeight + Style.marginL - } else if (root.barPosition === "right") { - maxX = parent.width - root.barMarginH - Style.barHeight - width - Style.marginL - } - - panelX = Math.max(minX, Math.min(panelX, maxX)) - calculatedX = panelX - } - } else { - // For horizontal bars, center panel on button X position - var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 - // Clamp to bar bounds (account for floating bar margins) - // When attached, panel should not extend beyond bar edges - if (couldAttach) { - // Inverted corners with horizontal direction extend left/right by radiusL - // When bar is floating, it also has rounded corners, so we need extra insets - var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 - var barLeftEdge = root.barMarginH + cornerInset - var barRightEdge = parent.width - root.barMarginH - cornerInset - panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width)) - } else { - panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) - } - calculatedX = panelX - } - } else { - - // Standard anchor positioning - Logger.d("NPanel", "Fallback to standard anchor positioning") - - if (root.panelAnchorHorizontalCenter) { - Logger.d("NPanel", " -> Horizontal center") - // Center horizontally, accounting for bar position and margins - if (root.barIsVertical) { - // For vertical bars, center in the available space not occupied by the bar - if (root.barPosition === "left") { - var availableStart = root.barMarginH + Style.barHeight - var availableWidth = parent.width - availableStart - calculatedX = availableStart + (availableWidth - width) / 2 - } else if (root.barPosition === "right") { - var availableWidth = parent.width - root.barMarginH - Style.barHeight - calculatedX = (availableWidth - width) / 2 - } else { - // No vertical bar, center normally - calculatedX = (parent.width - width) / 2 - } - } else { - // For horizontal bars or no bar, center normally - calculatedX = (parent.width - width) / 2 - } - } else if (effectivePanelAnchorRight) { - Logger.d("NPanel", " -> Right anchor") - // When attached to right vertical bar, position next to bar (like useButtonPosition does) - if (couldAttach && root.barIsVertical && root.barPosition === "right") { - var rightBarEdge = parent.width - root.barMarginH - Style.barHeight - calculatedX = rightBarEdge - width - } else if (couldAttach) { - // Attach to right screen edge - calculatedX = parent.width - width - } else { - // Detached: use margin - calculatedX = parent.width - width - Style.marginL - } - } else if (effectivePanelAnchorLeft) { - Logger.d("NPanel", " -> Left anchor") - // When attached to left vertical bar, position next to bar (like useButtonPosition does) - if (couldAttach && root.barIsVertical && root.barPosition === "left") { - var leftBarEdge = root.barMarginH + Style.barHeight - calculatedX = leftBarEdge - } else if (couldAttach) { - // Attach to left screen edge - calculatedX = 0 - } else { - // Detached: use margin - calculatedX = Style.marginL - } - } else { - // No explicit anchor: default to centering on bar - Logger.d("NPanel", " -> Default to center (no explicit anchor)") - - // For horizontal bars: center horizontally - // For vertical bars: center horizontally in available space - if (root.barIsVertical) { - // Center in the space not occupied by the bar - if (root.barPosition === "left") { - var availableStart = root.barMarginH + Style.barHeight - var availableWidth = parent.width - availableStart - Style.marginL - calculatedX = availableStart + (availableWidth - width) / 2 - } else { - // right - var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL - calculatedX = Style.marginL + (availableWidth - width) / 2 - } - } else { - // For horizontal bars: center horizontally, respect bar margins if attached - if (couldAttach) { - // When attached, respect bar bounds (like button position does) - var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) - var barLeftEdge = root.barMarginH + cornerInset - var barRightEdge = parent.width - root.barMarginH - cornerInset - var centeredX = (parent.width - width) / 2 - calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) - } else { - calculatedX = (parent.width - width) / 2 - } - } - } - } - - // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) - if (couldAttach && !root.barFloating && parent.width > 0 && width > 0) { - // Calculate edge positions accounting for bar position - // For vertical bars (left/right), we need to position panels AFTER the bar, not behind it - var leftEdgePos = root.barMarginH - if (root.barPosition === "left") { - // Bar is on the left, so left edge is after the bar - leftEdgePos = root.barMarginH + Style.barHeight - } - - var rightEdgePos = parent.width - root.barMarginH - width - if (root.barPosition === "right") { - // Bar is on the right, so right edge is before the bar - rightEdgePos = parent.width - root.barMarginH - Style.barHeight - width - } - - // Snap to left edge if within snap distance - if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { - calculatedX = leftEdgePos - } // Snap to right edge if within snap distance - else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) { - calculatedX = rightEdgePos - } - } - - return calculatedX - } - - y: { - var calculatedY - - // If useButtonPosition is enabled, position panel relative to bar - // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid - // after the source panel (e.g., ControlCenter) closes, but we still have valid position data - if (root.useButtonPosition && parent.height > 0 && height > 0) { - if (root.barPosition === "top") { - // Panel below top bar - var topBarEdge = root.barMarginV + Style.barHeight - if (couldAttach) { - // Panel sits right at bar edge (inverted corners align perfectly) - calculatedY = topBarEdge - } else { - calculatedY = topBarEdge + Style.marginM - } - } else if (root.barPosition === "bottom") { - // Panel above bottom bar - var bottomBarEdge = parent.height - root.barMarginV - Style.barHeight - if (couldAttach) { - // Panel sits right at bar edge (inverted corners align perfectly) - calculatedY = bottomBarEdge - height - } else { - calculatedY = bottomBarEdge - height - Style.marginM - } - } else if (root.barIsVertical) { - // For vertical bars, center panel on button Y position - var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2 - // Clamp to bar bounds (account for floating bar margins and inverted corners) - var extraPadding = (couldAttach && root.barFloating) ? Style.radiusL : 0 - if (couldAttach) { - // When attached, panel should not extend beyond bar edges (accounting for floating margins) - // Inverted corners with vertical direction extend up/down by radiusL - // When bar is floating, it also has rounded corners, so we need extra inset - var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0) - var barTopEdge = root.barMarginV + cornerInset - var barBottomEdge = parent.height - root.barMarginV - cornerInset - panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - height)) - } else { - panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding)) - } - calculatedY = panelY - } - } else { - - // Standard anchor positioning - // Calculate bar offset for detached panels - they should never overlap the bar - var barOffset = 0 - if (!couldAttach) { - // For detached panels, always account for bar position - if (root.barPosition === "top") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM - } else if (root.barPosition === "bottom") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM - } - } else { - // For attached panels with explicit anchors - if (effectivePanelAnchorTop && root.barPosition === "top") { - // When attached to top bar: position right at bar edge (inverted corners align perfectly) - calculatedY = root.barMarginV + Style.barHeight - } else if (effectivePanelAnchorBottom && root.barPosition === "bottom") { - // When attached to bottom bar: position right at bar edge (inverted corners align perfectly) - calculatedY = parent.height - root.barMarginV - Style.barHeight - height - } else if (!root.hasExplicitVerticalAnchor) { - // No explicit vertical anchor AND attached: default to attaching to bar edge - if (root.barPosition === "top") { - // Attach to top bar - calculatedY = root.barMarginV + Style.barHeight - } else if (root.barPosition === "bottom") { - // Attach to bottom bar - calculatedY = parent.height - root.barMarginV - Style.barHeight - height - } - // For vertical bars with no explicit anchor: fall through to center vertically on bar - } - } - - // Continue if calculatedY was already set above, or proceed with anchor positioning - if (calculatedY === undefined) { - if (root.panelAnchorVerticalCenter) { - // Center vertically, accounting for bar position and margins - if (!root.barIsVertical) { - // For horizontal bars, center in the available space not occupied by the bar - if (root.barPosition === "top") { - var availableStart = root.barMarginV + Style.barHeight - var availableHeight = parent.height - availableStart - calculatedY = availableStart + (availableHeight - height) / 2 - } else if (root.barPosition === "bottom") { - var availableHeight = parent.height - root.barMarginV - Style.barHeight - calculatedY = (availableHeight - height) / 2 - } else { - // No horizontal bar, center normally - calculatedY = (parent.height - height) / 2 - } - } else { - // For vertical bars or no bar, center normally - calculatedY = (parent.height - height) / 2 - } - } else if (effectivePanelAnchorTop) { - // When couldAttach=true, attach to top screen edge; otherwise use margin - if (couldAttach) { - calculatedY = 0 - } else { - // Only apply barOffset if bar is also at top (to avoid overlapping) - var topBarOffset = (root.barPosition === "top") ? barOffset : 0 - calculatedY = topBarOffset + Style.marginL - } - } else if (effectivePanelAnchorBottom) { - // When couldAttach=true, attach to bottom screen edge; otherwise use margin - if (couldAttach) { - calculatedY = parent.height - height - } else { - // Only apply barOffset if bar is also at bottom (to avoid overlapping) - var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0 - calculatedY = parent.height - height - bottomBarOffset - Style.marginL - } - } else { - // No explicit vertical anchor - if (root.barIsVertical) { - // For vertical bars: center vertically on bar - if (couldAttach) { - // When attached, respect bar bounds - var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 - var barTopEdge = root.barMarginV + cornerInset - var barBottomEdge = parent.height - root.barMarginV - cornerInset - var centeredY = (parent.height - height) / 2 - calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) - } else { - calculatedY = (parent.height - height) / 2 - } - } else { - // For horizontal bars: attach to bar edge by default - if (couldAttach && !root.barIsVertical) { - if (root.barPosition === "top") { - calculatedY = root.barMarginV + Style.barHeight - } else if (root.barPosition === "bottom") { - calculatedY = parent.height - root.barMarginV - Style.barHeight - height - } - } else { - // Detached or no bar position: use default positioning - if (root.barPosition === "top") { - calculatedY = barOffset + Style.marginL - } else if (root.barPosition === "bottom") { - calculatedY = Style.marginL - } else { - calculatedY = Style.marginL - } - } - } - } - } - } - - // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) - if (couldAttach && !root.barFloating && parent.height > 0 && height > 0) { - // Calculate edge positions accounting for bar position - // For horizontal bars (top/bottom), we need to position panels AFTER the bar, not behind it - var topEdgePos = root.barMarginV - if (root.barPosition === "top") { - // Bar is on the top, so top edge is after the bar - topEdgePos = root.barMarginV + Style.barHeight - } - - var bottomEdgePos = parent.height - root.barMarginV - height - if (root.barPosition === "bottom") { - // Bar is on the bottom, so bottom edge is before the bar - bottomEdgePos = parent.height - root.barMarginV - Style.barHeight - height - } - - // Snap to top edge if within snap distance - if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { - calculatedY = topEdgePos - } // Snap to bottom edge if within snap distance - else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { - calculatedY = bottomEdgePos - } - } - - return calculatedY - } - - // MouseArea to catch clicks on the panel and prevent them from reaching the background - // This prevents closing the panel when clicking inside it - MouseArea { - anchors.fill: parent - z: -1 // Behind content, but on the panel background - onClicked: { - - // Accept and ignore - prevents propagation to background - } - } - - // Panel content loader - Loader { - id: contentLoader - anchors.fill: parent - sourceComponent: root.panelContent - } - } - } - } - } -} diff --git a/shell.qml b/shell.qml index c78025d95..d5e9cd6ba 100644 --- a/shell.qml +++ b/shell.qml @@ -18,30 +18,14 @@ import qs.Commons import qs.Services import qs.Widgets -// Core Modules +// Panel Windows import qs.Modules.Background import qs.Modules.Dock +import qs.Modules.MainScreen import qs.Modules.LockScreen -import qs.Modules.SessionMenu - -// Bar & Bar Components -import qs.Modules.Bar -import qs.Modules.Bar.Extras -import qs.Modules.Bar.Audio -import qs.Modules.Bar.Bluetooth -import qs.Modules.Bar.Battery -import qs.Modules.Bar.Calendar -import qs.Modules.Bar.WiFi - -// Panels & UI Components -import qs.Modules.ControlCenter -import qs.Modules.Launcher import qs.Modules.Notification import qs.Modules.OSD -import qs.Modules.Settings import qs.Modules.Toast -import qs.Modules.Wallpaper -import qs.Modules.SetupWizard ShellRoot { id: shellRoot @@ -74,89 +58,6 @@ ShellRoot { settingsLoaded = true } } - - // ------------------------------ - // Define panel components (must be at ShellRoot level for NFullScreenWindow access) - Component { - id: launcherComponent - Launcher {} - } - - Component { - id: controlCenterComponent - ControlCenterPanel {} - } - - Component { - id: trayDropdownComponent - TrayDropdownPanel {} - } - - Component { - id: trayMenuComponent - TrayMenu {} - } - - Component { - id: calendarComponent - CalendarPanel {} - } - - Component { - id: settingsComponent - SettingsPanel {} - } - - Component { - id: directWidgetSettingsComponent - DirectWidgetSettingsPanel {} - } - - Component { - id: notificationHistoryComponent - NotificationHistoryPanel {} - } - - Component { - id: sessionMenuComponent - SessionMenu {} - } - - Component { - id: wifiComponent - WiFiPanel {} - } - - Component { - id: bluetoothComponent - BluetoothPanel {} - } - - Component { - id: audioComponent - AudioPanel {} - } - - Component { - id: wallpaperComponent - WallpaperPanel {} - } - - Component { - id: batteryComponent - BatteryPanel {} - } - - Component { - id: setupWizardComponent - SetupWizard {} - } - - Component { - id: barComp - Bar {} - } - Loader { active: i18nLoaded && settingsLoaded @@ -179,11 +80,13 @@ ShellRoot { DistroService.init() } - Background {} Overview {} - + Background {} Dock {} + ToastOverlay {} + OSD {} + Notification { id: notification } @@ -196,9 +99,6 @@ ShellRoot { } } - ToastOverlay {} - OSD {} - // IPCService is treated as a service // but it's actually an Item that needs to exists in the shell. IPCService {} @@ -206,7 +106,7 @@ ShellRoot { } // ------------------------------ - // NFullScreenWindow for each screen (manages bar + all panels) + // MainScreen for each screen (manages bar + all panels) // Wrapped in Loader to optimize memory - only loads when screen needs it Variants { model: Quickshell.screens @@ -214,12 +114,8 @@ ShellRoot { required property ShellScreen modelData property bool shouldBeActive: { - if (!i18nLoaded || !settingsLoaded) - return false - if (!modelData || !modelData.name) - return false - - Logger.d("Shell", "NFullScreenWindow activated for", modelData?.name) + if (!i18nLoaded || !settingsLoaded || !modelData || !modelData.name) + Logger.d("Shell", "MainScreen activated for", modelData?.name) return true } @@ -237,63 +133,12 @@ ShellRoot { parent.windowLoaded = true } - sourceComponent: NFullScreenWindow { + sourceComponent: MainScreen { screen: windowLoader.loaderScreen - - // Register all panel components - panelComponents: [{ - "id": "launcherPanel", - "component": launcherComponent - }, { - "id": "controlCenterPanel", - "component": controlCenterComponent - }, { - "id": "trayDropdownPanel", - "component": trayDropdownComponent - }, { - "id": "trayMenu", - "component": trayMenuComponent - }, { - "id": "calendarPanel", - "component": calendarComponent - }, { - "id": "settingsPanel", - "component": settingsComponent - }, { - "id": "directWidgetSettingsPanel", - "component": directWidgetSettingsComponent - }, { - "id": "notificationHistoryPanel", - "component": notificationHistoryComponent - }, { - "id": "sessionMenuPanel", - "component": sessionMenuComponent - }, { - "id": "wifiPanel", - "component": wifiComponent - }, { - "id": "bluetoothPanel", - "component": bluetoothComponent - }, { - "id": "audioPanel", - "component": audioComponent - }, { - "id": "wallpaperPanel", - "component": wallpaperComponent - }, { - "id": "batteryPanel", - "component": batteryComponent - }, { - "id": "setupWizardPanel", - "component": setupWizardComponent - }] - - // Bar component - barComponent: barComp } } - // BarExclusionZone - created after NFullScreenWindow has fully loaded + // BarExclusionZone - created after MainScreen has fully loaded // Disabled when bar is hidden or not configured for this screen Loader { active: {