diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index f90a6512..f3bac6f0 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import QtQuick.Effects import Quickshell import Quickshell.Wayland @@ -24,6 +25,7 @@ import qs.Modules.Panels.SetupWizard import qs.Modules.Panels.Tray import qs.Modules.Panels.Wallpaper import qs.Modules.Panels.WiFi +import qs.Services.Compositor import qs.Services.UI /** @@ -49,22 +51,22 @@ PanelWindow { readonly property alias wallpaperPanel: wallpaperPanel readonly property alias wifiPanel: wifiPanel - // Expose panel placeholders for AllBackgrounds - readonly property var audioPanelPlaceholder: audioPanel.panelPlaceholder - readonly property var batteryPanelPlaceholder: batteryPanel.panelPlaceholder - readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelPlaceholder - readonly property var brightnessPanelPlaceholder: brightnessPanel.panelPlaceholder - readonly property var calendarPanelPlaceholder: calendarPanel.panelPlaceholder - readonly property var changelogPanelPlaceholder: changelogPanel.panelPlaceholder - readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelPlaceholder - readonly property var launcherPanelPlaceholder: launcherPanel.panelPlaceholder - readonly property var notificationHistoryPanelPlaceholder: notificationHistoryPanel.panelPlaceholder - readonly property var sessionMenuPanelPlaceholder: sessionMenuPanel.panelPlaceholder - readonly property var settingsPanelPlaceholder: settingsPanel.panelPlaceholder - readonly property var setupWizardPanelPlaceholder: setupWizardPanel.panelPlaceholder - readonly property var trayDrawerPanelPlaceholder: trayDrawerPanel.panelPlaceholder - readonly property var wallpaperPanelPlaceholder: wallpaperPanel.panelPlaceholder - readonly property var wifiPanelPlaceholder: wifiPanel.panelPlaceholder + // Expose panel backgrounds for AllBackgrounds + readonly property var audioPanelPlaceholder: audioPanel.panelRegion + readonly property var batteryPanelPlaceholder: batteryPanel.panelRegion + readonly property var bluetoothPanelPlaceholder: bluetoothPanel.panelRegion + readonly property var brightnessPanelPlaceholder: brightnessPanel.panelRegion + readonly property var calendarPanelPlaceholder: calendarPanel.panelRegion + readonly property var changelogPanelPlaceholder: changelogPanel.panelRegion + readonly property var controlCenterPanelPlaceholder: controlCenterPanel.panelRegion + readonly property var launcherPanelPlaceholder: launcherPanel.panelRegion + readonly property var notificationHistoryPanelPlaceholder: notificationHistoryPanel.panelRegion + readonly property var sessionMenuPanelPlaceholder: sessionMenuPanel.panelRegion + readonly property var settingsPanelPlaceholder: settingsPanel.panelRegion + readonly property var setupWizardPanelPlaceholder: setupWizardPanel.panelRegion + readonly property var trayDrawerPanelPlaceholder: trayDrawerPanel.panelRegion + readonly property var wallpaperPanelPlaceholder: wallpaperPanel.panelRegion + readonly property var wifiPanelPlaceholder: wifiPanel.panelRegion Component.onCompleted: { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y); @@ -74,7 +76,17 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Top WlrLayershell.namespace: "noctalia-background-" + (screen?.name || "unknown") WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: { + if (!root.isPanelOpen) { + return WlrKeyboardFocus.None; + } + if (CompositorService.isHyprland) { + // Exclusive focus on hyprland is too restrictive. + return WlrKeyboardFocus.OnDemand; + } else { + return PanelService.openedPanel.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand; + } + } anchors { top: true @@ -128,7 +140,9 @@ PanelWindow { height: root.height intersection: Intersection.Xor - regions: [barMaskRegion] + // Only include regions that are actually needed + // panelRegions is handled by PanelService, bar is local to this screen + regions: [barMaskRegion, backgroundMaskRegion] // Bar region - subtract bar area from mask (only if bar should be shown on this screen) Region { @@ -142,6 +156,16 @@ PanelWindow { height: root.barShouldShow ? barPlaceholder.height : 0 intersection: Intersection.Subtract } + + // Background region for click-to-close - reactive sizing + Region { + id: backgroundMaskRegion + x: 0 + y: 0 + width: root.isPanelOpen && !isPanelClosing ? root.width : 0 + height: root.isPanelOpen && !isPanelClosing ? root.height : 0 + intersection: Intersection.Subtract + } } // -------------------------------------- @@ -162,6 +186,20 @@ PanelWindow { 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 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (PanelService.openedPanel) { + PanelService.openedPanel.close(); + } + } + z: 0 // Behind panels and bar + } + // --------------------------------------- // All panels always exist // --------------------------------------- @@ -169,90 +207,105 @@ PanelWindow { id: audioPanel objectName: "audioPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } BatteryPanel { id: batteryPanel objectName: "batteryPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } BluetoothPanel { id: bluetoothPanel objectName: "bluetoothPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } BrightnessPanel { id: brightnessPanel objectName: "brightnessPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } ControlCenterPanel { id: controlCenterPanel objectName: "controlCenterPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } ChangelogPanel { id: changelogPanel objectName: "changelogPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } CalendarPanel { id: calendarPanel objectName: "calendarPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } Launcher { id: launcherPanel objectName: "launcherPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } NotificationHistoryPanel { id: notificationHistoryPanel objectName: "notificationHistoryPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } SessionMenu { id: sessionMenuPanel objectName: "sessionMenuPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } SettingsPanel { id: settingsPanel objectName: "settingsPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } SetupWizard { id: setupWizardPanel objectName: "setupWizardPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } TrayDrawerPanel { id: trayDrawerPanel objectName: "trayDrawerPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } WallpaperPanel { id: wallpaperPanel objectName: "wallpaperPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } WiFiPanel { id: wifiPanel objectName: "wifiPanel-" + (root.screen?.name || "unknown") screen: root.screen + z: 50 } // ---------------------------------------------- @@ -358,4 +411,183 @@ PanelWindow { */ ScreenCorners {} } + + // ======================================== + // Centralized Keyboard Shortcuts + // ======================================== + // These shortcuts delegate to the opened panel's handler functions + // Panels can implement: onEscapePressed, onTabPressed, onShiftTabPressed, + // onUpPressed, onDownPressed, onReturnPressed + + 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: "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(); + } + } + } + + 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: "Backtab" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onBackTabPressed) { + PanelService.openedPanel.onBackTabPressed(); + } + } + } + + 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: "Ctrl+N" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlNPressed) { + PanelService.openedPanel.onCtrlNPressed(); + } + } + } + + Shortcut { + sequence: "Ctrl+P" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlPPressed) { + PanelService.openedPanel.onCtrlPPressed(); + } + } + } } diff --git a/Modules/MainScreen/PanelPlaceholder.qml b/Modules/MainScreen/PanelPlaceholder.qml deleted file mode 100644 index 405be4de..00000000 --- a/Modules/MainScreen/PanelPlaceholder.qml +++ /dev/null @@ -1,706 +0,0 @@ -import QtQuick -import Quickshell -import qs.Commons -import qs.Services.UI - -/** -* PanelPlaceholder - Lightweight positioning logic for panel backgrounds -* -* This component stays in MainScreen and provides geometry for PanelBackground rendering. -* It contains only positioning calculations and animations, no visual content. -* The actual panel content lives in a separate SmartPanelWindow. -*/ -Item { - id: root - - // Required properties - required property ShellScreen screen - required property string panelName - // Unique identifier - - // Panel size properties - property real preferredWidth: 700 - property real preferredHeight: 900 - property real preferredWidthRatio - property real preferredHeightRatio - property var buttonItem: null - property bool forceAttachToBar: false - - // 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 distance - property real edgeSnapDistance: 50 - - // State tracking (controlled by SmartPanelWindow) - property bool isPanelVisible: false - property bool isClosing: false - property bool opacityFadeComplete: false - property bool sizeAnimationComplete: false - - // Derived state: track opening transition - readonly property bool isOpening: isPanelVisible && !isClosing && !sizeAnimationComplete - - // Content size (set by SmartPanelWindow when content size changes) - property real contentPreferredWidth: 0 - property real contentPreferredHeight: 0 - - // Expose panelBackground as panelItem for AllBackgrounds - readonly property var panelItem: panelBackground - - // Bar configuration - 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 - - // Attachment properties - readonly property bool allowAttach: Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar - readonly property bool allowAttachToBar: { - if (!(Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar) || 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 (depend on allowAttach) - readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") || (allowAttach && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical) - readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (allowAttach && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical) - readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (allowAttach && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical) - readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (allowAttach && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical) - - // Panel dimensions and visibility - visible: isPanelVisible - width: parent ? parent.width : 0 - height: parent ? parent.height : 0 - - // Update position when UI scale changes - Connections { - target: Style - - function onUiScaleRatioChanged() { - if (root.isPanelVisible) { - root.setPosition(); - } - } - } - - // Public function to update content size from SmartPanelWindow - function updateContentSize(w, h) { - contentPreferredWidth = w; - contentPreferredHeight = h; - if (isPanelVisible) { - setPosition(); - } - } - - // Main positioning calculation function - function setPosition() { - // Don't calculate position if parent dimensions aren't available yet - if (!root.width || !root.height) { - Logger.d("PanelPlaceholder", "Skipping setPosition - dimensions not ready:", root.width, "x", root.height, panelName); - Qt.callLater(setPosition); - return; - } - - // Calculate panel dimensions first (needed for positioning) - var w; - // Priority 1: Content-driven size (dynamic) - if (contentPreferredWidth > 0) { - w = contentPreferredWidth; - } // Priority 2: Ratio-based size - else if (root.preferredWidthRatio !== undefined) { - w = Math.round(Math.max(root.width * root.preferredWidthRatio, root.preferredWidth)); - } // Priority 3: Static preferred width - else { - w = root.preferredWidth; - } - var panelWidth = Math.min(w, root.width - Style.marginL * 2); - - var h; - // Priority 1: Content-driven size (dynamic) - if (contentPreferredHeight > 0) { - h = contentPreferredHeight; - } // Priority 2: Ratio-based size - else if (root.preferredHeightRatio !== undefined) { - h = Math.round(Math.max(root.height * root.preferredHeightRatio, root.preferredHeight)); - } // Priority 3: Static preferred height - else { - h = root.preferredHeight; - } - var panelHeight = Math.min(h, root.height - Style.barHeight - Style.marginL * 2); - - // Update panelBackground target size (will be animated) - panelBackground.targetWidth = panelWidth; - panelBackground.targetHeight = panelHeight; - - // Calculate position - var calculatedX; - var calculatedY; - - // ===== X POSITIONING ===== - if (root.useButtonPosition && root.width > 0 && panelWidth > 0) { - if (root.barIsVertical) { - // For vertical bars - if (allowAttach) { - // 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 (allowAttach) { - 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 (allowAttach && root.barIsVertical && root.barPosition === "right") { - var rightBarEdge = root.width - root.barMarginH - Style.barHeight; - calculatedX = rightBarEdge - panelWidth; - } else if (allowAttach) { - // Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar - var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); - if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { - var rightCornerInset = Style.radiusL * 2; - calculatedX = root.width - root.barMarginH - rightCornerInset - panelWidth; - } else { - calculatedX = root.width - panelWidth; - } - } else { - calculatedX = root.width - panelWidth - Style.marginL; - } - } else if (root.effectivePanelAnchorLeft) { - if (allowAttach && root.barIsVertical && root.barPosition === "left") { - var leftBarEdge = root.barMarginH + Style.barHeight; - calculatedX = leftBarEdge; - } else if (allowAttach) { - // Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar - var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); - if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { - var leftCornerInset = Style.radiusL * 2; - calculatedX = root.barMarginH + leftCornerInset; - } else { - 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 (allowAttach) { - 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 (allowAttach && !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; - } - - // Only snap to left edge if panel is actually meant to be at left - var shouldSnapToLeft = root.effectivePanelAnchorLeft || (!root.hasExplicitHorizontalAnchor && root.barPosition === "left"); - // Only snap to right edge if panel is actually meant to be at right - var shouldSnapToRight = root.effectivePanelAnchorRight || (!root.hasExplicitHorizontalAnchor && root.barPosition === "right"); - - if (shouldSnapToLeft && Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { - calculatedX = leftEdgePos; - } else if (shouldSnapToRight && 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 (allowAttach) { - calculatedY = topBarEdge; - } else { - calculatedY = topBarEdge + Style.marginM; - } - } else if (root.barPosition === "bottom") { - var bottomBarEdge = root.height - root.barMarginV - Style.barHeight; - if (allowAttach) { - 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 = (allowAttach && root.barFloating) ? Style.radiusL : 0; - if (allowAttach) { - 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 (!allowAttach) { - 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 (allowAttach) { - calculatedY = 0; - } else { - var topBarOffset = (root.barPosition === "top") ? barOffset : 0; - calculatedY = topBarOffset + Style.marginL; - } - } else if (root.effectivePanelAnchorBottom) { - if (allowAttach) { - calculatedY = root.height - panelHeight; - } else { - var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0; - calculatedY = root.height - panelHeight - bottomBarOffset - Style.marginL; - } - } else { - if (root.barIsVertical) { - if (allowAttach) { - 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 (allowAttach && !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 (allowAttach && !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; - } - - // Only snap to top edge if panel is actually meant to be at top - var shouldSnapToTop = root.effectivePanelAnchorTop || (!root.hasExplicitVerticalAnchor && root.barPosition === "top"); - // Only snap to bottom edge if panel is actually meant to be at bottom - var shouldSnapToBottom = root.effectivePanelAnchorBottom || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom"); - - if (shouldSnapToTop && Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { - calculatedY = topEdgePos; - } else if (shouldSnapToBottom && Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { - calculatedY = bottomEdgePos; - } - } - - // Apply calculated positions (set targets for animation) - panelBackground.targetX = calculatedX; - panelBackground.targetY = calculatedY; - - Logger.d("PanelPlaceholder", "Position calculated:", calculatedX, calculatedY, panelName); - Logger.d("PanelPlaceholder", " Panel size:", panelWidth, "x", panelHeight); - } - - // The panel background geometry item - Item { - id: panelBackground - - // Store target dimensions (set by setPosition()) - property real targetWidth: root.preferredWidth - property real targetHeight: root.preferredHeight - property real targetX: 0 - property real targetY: 0 - - property var bezierCurve: [0.05, 0, 0.133, 0.06, 0.166, 0.4, 0.208, 0.82, 0.25, 1, 1, 1] - - // Edge detection - readonly property bool touchingLeftEdge: allowAttach && panelBackground.x <= 1 - readonly property bool touchingRightEdge: allowAttach && (panelBackground.x + panelBackground.width) >= (root.width - 1) - readonly property bool touchingTopEdge: allowAttach && panelBackground.y <= 1 - readonly property bool touchingBottomEdge: allowAttach && (panelBackground.y + panelBackground.height) >= (root.height - 1) - - // Bar edge detection - readonly property bool touchingTopBar: allowAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.y - (root.barMarginV + Style.barHeight)) <= 1 - readonly property bool touchingBottomBar: allowAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((panelBackground.y + panelBackground.height) - (root.height - root.barMarginV - Style.barHeight)) <= 1 - readonly property bool touchingLeftBar: allowAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(panelBackground.x - (root.barMarginH + Style.barHeight)) <= 1 - readonly property bool touchingRightBar: allowAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((panelBackground.x + panelBackground.width) - (root.width - root.barMarginH - Style.barHeight)) <= 1 - - // Animation direction determination (using target position to avoid binding loops) - readonly property bool willTouchTopBar: { - if (!isPanelVisible) - return false; - if (!allowAttachToBar || root.barPosition !== "top" || root.barIsVertical) - return false; - var targetTopBarY = root.barMarginV + Style.barHeight; - return Math.abs(panelBackground.targetY - targetTopBarY) <= 1; - } - readonly property bool willTouchBottomBar: { - if (!isPanelVisible) - return false; - if (!allowAttachToBar || root.barPosition !== "bottom" || root.barIsVertical) - return false; - var targetBottomBarY = root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight; - return Math.abs(panelBackground.targetY - targetBottomBarY) <= 1; - } - readonly property bool willTouchLeftBar: { - if (!isPanelVisible) - return false; - if (!allowAttachToBar || root.barPosition !== "left" || !root.barIsVertical) - return false; - var targetLeftBarX = root.barMarginH + Style.barHeight; - return Math.abs(panelBackground.targetX - targetLeftBarX) <= 1; - } - readonly property bool willTouchRightBar: { - if (!isPanelVisible) - return false; - if (!allowAttachToBar || root.barPosition !== "right" || !root.barIsVertical) - return false; - var targetRightBarX = root.width - root.barMarginH - Style.barHeight - panelBackground.targetWidth; - return Math.abs(panelBackground.targetX - targetRightBarX) <= 1; - } - readonly property bool willTouchTopEdge: isPanelVisible && allowAttach && panelBackground.targetY <= 1 - readonly property bool willTouchBottomEdge: isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) - readonly property bool willTouchLeftEdge: isPanelVisible && allowAttach && panelBackground.targetX <= 1 - readonly property bool willTouchRightEdge: isPanelVisible && allowAttach && (panelBackground.targetX + panelBackground.targetWidth) >= (root.width - 1) - - readonly property bool isActuallyAttachedToAnyEdge: { - if (!isPanelVisible) - return false; - return willTouchTopBar || willTouchBottomBar || willTouchLeftBar || willTouchRightBar || willTouchTopEdge || willTouchBottomEdge || willTouchLeftEdge || willTouchRightEdge; - } - - readonly property bool animateFromTop: { - if (!isPanelVisible) - return true; - if (willTouchTopBar) - return true; - if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) - return true; - if (!isActuallyAttachedToAnyEdge) - return true; - return false; - } - readonly property bool animateFromBottom: { - if (!isPanelVisible) - return false; - if (willTouchBottomBar) - return true; - if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) - return true; - return false; - } - readonly property bool animateFromLeft: { - if (!isPanelVisible) - return false; - if (willTouchTopBar || willTouchBottomBar) - return false; - if (willTouchLeftBar) - return true; - var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1; - var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1); - if (touchingTopEdge || touchingBottomEdge) - return false; - if (willTouchLeftEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) - return true; - return false; - } - readonly property bool animateFromRight: { - if (!isPanelVisible) - return false; - if (willTouchTopBar || willTouchBottomBar) - return false; - if (willTouchRightBar) - return true; - var touchingTopEdge = isPanelVisible && allowAttach && panelBackground.targetY <= 1; - var touchingBottomEdge = isPanelVisible && allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1); - if (touchingTopEdge || touchingBottomEdge) - return false; - if (willTouchRightEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) - return true; - return false; - } - - readonly property bool shouldAnimateWidth: !shouldAnimateHeight && (animateFromLeft || animateFromRight) - readonly property bool shouldAnimateHeight: animateFromTop || animateFromBottom - - // Track whether we're in an initial open/close state transition vs normal content resizing - readonly property bool isStateTransition: root.isOpening || root.isClosing - - // Current animated width/height - readonly property real currentWidth: { - if (isClosing && opacityFadeComplete && shouldAnimateWidth) - return 0; - if (isClosing || isPanelVisible) - return targetWidth; - return 0; - } - readonly property real currentHeight: { - if (isClosing && opacityFadeComplete && shouldAnimateHeight) - return 0; - if (isClosing || isPanelVisible) - return targetHeight; - return 0; - } - - width: currentWidth - height: currentHeight - - x: { - if (animateFromRight) { - if (isPanelVisible || isClosing) { - var targetRightEdge = targetX + targetWidth; - return targetRightEdge - width; - } - } - return targetX; - } - y: { - if (animateFromBottom) { - if (isPanelVisible || isClosing) { - var targetBottomEdge = targetY + targetHeight; - return targetBottomEdge - height; - } - } - return targetY; - } - - Behavior on width { - NumberAnimation { - // During opening: use 0ms if not animating width, otherwise use normal duration - // During closing: use 0ms if not animating width, otherwise use fast duration - // During normal content resizing: always use normal duration - duration: (root.isOpening && !panelBackground.shouldAnimateWidth) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateWidth) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal - easing.type: Easing.BezierSpline - easing.bezierCurve: panelBackground.bezierCurve - } - } - - Behavior on height { - NumberAnimation { - // During opening: use 0ms if not animating height, otherwise use normal duration - // During closing: use 0ms if not animating height, otherwise use fast duration - // During normal content resizing: always use normal duration - duration: (root.isOpening && !panelBackground.shouldAnimateHeight) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateHeight) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal - easing.type: Easing.BezierSpline - easing.bezierCurve: panelBackground.bezierCurve - } - } - - // Corner states for PanelBackground to read - property int topLeftCornerState: { - var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)); - var barTouchInverted = touchingTopBar || touchingLeftBar; - var edgeInverted = allowAttach && (touchingLeftEdge || touchingTopEdge); - var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top"); - - if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { - if (touchingLeftEdge && touchingTopEdge) - return 0; - if (touchingLeftEdge) - return 2; - if (touchingTopEdge) - return 1; - return root.barIsVertical ? 2 : 1; - } - return 0; - } - - property int topRightCornerState: { - var barInverted = allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)); - var barTouchInverted = touchingTopBar || touchingRightBar; - var edgeInverted = allowAttach && (touchingRightEdge || touchingTopEdge); - var oppositeEdgeInverted = allowAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top"); - - if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { - if (touchingRightEdge && touchingTopEdge) - return 0; - if (touchingRightEdge) - return 2; - if (touchingTopEdge) - return 1; - return root.barIsVertical ? 2 : 1; - } - return 0; - } - - property int bottomLeftCornerState: { - var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)); - var barTouchInverted = touchingBottomBar || touchingLeftBar; - var edgeInverted = allowAttach && (touchingLeftEdge || touchingBottomEdge); - var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom"); - - if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { - if (touchingLeftEdge && touchingBottomEdge) - return 0; - if (touchingLeftEdge) - return 2; - if (touchingBottomEdge) - return 1; - return root.barIsVertical ? 2 : 1; - } - return 0; - } - - property int bottomRightCornerState: { - var barInverted = allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)); - var barTouchInverted = touchingBottomBar || touchingRightBar; - var edgeInverted = allowAttach && (touchingRightEdge || touchingBottomEdge); - var oppositeEdgeInverted = allowAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom"); - - if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { - if (touchingRightEdge && touchingBottomEdge) - return 0; - if (touchingRightEdge) - return 2; - if (touchingBottomEdge) - return 1; - return root.barIsVertical ? 2 : 1; - } - return 0; - } - } -} diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml index 25cc8641..45143c04 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -4,12 +4,7 @@ import qs.Commons import qs.Services.UI /** -* SmartPanel - Wrapper that creates placeholder + content window -* -* This component is a thin wrapper that maintains backward compatibility -* while splitting panel rendering into: -* 1. PanelPlaceholder (in MainScreen, for background rendering) -* 2. SmartPanelWindow (separate window, for content) +* SmartPanel for use within MainScreen */ Item { id: root @@ -25,6 +20,8 @@ Item { property real preferredHeight: 900 property real preferredWidthRatio property real preferredHeightRatio + property color panelBackgroundColor: Color.mSurface + property color panelBorderColor: Color.mOutline property var buttonItem: null property bool forceAttachToBar: false @@ -36,33 +33,50 @@ Item { property bool panelAnchorLeft: false property bool panelAnchorRight: false - // Keyboard focus - property bool exclusiveKeyboard: true + // Button position properties + property bool useButtonPosition: false + property point buttonPosition: Qt.point(0, 0) + property int buttonWidth: 0 + property int buttonHeight: 0 - // Support close with escape + // 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 + + // Track size animation completion for sequential opacity animation + property bool sizeAnimationComplete: false + + // Derived state: track opening transition + readonly property bool isOpening: isPanelVisible && !isClosing && !sizeAnimationComplete + + // Track close animation state: fade opacity first, then shrink size + property bool isClosing: false + property bool opacityFadeComplete: false + property bool closeFinalized: false // Prevent double-finalization + + // Safety: Watchdog timers to prevent stuck states + property bool closeWatchdogActive: false + property bool openWatchdogActive: false + + // Close with escape key property bool closeWithEscape: true - // Track if window should be active (for lazy loading and cleanup) - property bool windowActive: false + property bool exclusiveKeyboard: true - // Expose panel state (from content window) - readonly property bool isPanelOpen: windowLoader.item ? windowLoader.item.isPanelOpen : false - readonly property bool isPanelVisible: windowLoader.item ? windowLoader.item.isPanelVisible : false - - // Expose panelRegion for backward compatibility (MainScreen mask) - readonly property var panelRegion: panelPlaceholder ? panelPlaceholder.panelItem : null - - // Signals - signal opened - signal closed - - // Keyboard event handlers - these can be overridden by panel implementations - // Note: SmartPanelWindow directly calls these functions via panelWrapper reference + // Keyboard event handlers - override these in specific panels to handle shortcuts + // These are called from MainScreen's centralized shortcuts function onEscapePressed() { + if (closeWithEscape) + close(); } function onTabPressed() { } - function onBackTabPressed() { + function onShiftTabPressed() { } function onUpPressed() { } @@ -87,99 +101,1021 @@ Item { function onCtrlKPressed() { } - // Public control functions + // 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 allowAttach) + // 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.allowAttach && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical) + readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (panelContent.allowAttach && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical) + readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (panelContent.allowAttach && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical) + readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (panelContent.allowAttach && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical) + + signal opened + signal closed + + Connections { + target: Style + + function onUiScaleRatioChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition(); + } + } + } + + // Panel visibility and sizing + visible: isPanelVisible + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + + // Panel control functions function toggle(buttonItem, buttonName) { - // Ensure window is created before toggling - if (!root.windowActive) { - root.windowActive = true; - Qt.callLater(function () { - if (windowLoader.item) { - windowLoader.item.toggle(buttonItem, buttonName); - } - }); - } else if (windowLoader.item) { - windowLoader.item.toggle(buttonItem, buttonName); + if (!isPanelOpen) { + open(buttonItem, buttonName); + } else { + close(); } } function open(buttonItem, buttonName) { - // Ensure window is created before opening - if (!root.windowActive) { - root.windowActive = true; - Qt.callLater(function () { - if (windowLoader.item) { - windowLoader.item.open(buttonItem, buttonName); - } - }); - } else if (windowLoader.item) { - windowLoader.item.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() { - if (windowLoader.item) { - windowLoader.item.close(); + // Start close sequence: fade opacity first + isClosing = true; + sizeAnimationComplete = false; + closeFinalized = false; + + // Stop the open animation timer if it's still running + opacityTrigger.stop(); + openWatchdogActive = false; + openWatchdogTimer.stop(); + + // Start close watchdog timer + closeWatchdogActive = true; + closeWatchdogTimer.restart(); + + // If opacity is already 0 (closed during open animation before fade-in), + // skip directly to size animation + if (root.opacity === 0.0) { + opacityFadeComplete = true; + } else { + opacityFadeComplete = false; } + + // Opacity will fade out, then size will shrink, then finalizeClose() will complete + Logger.d("SmartPanel", "Closing panel", objectName); + } + + function finalizeClose() { + // Prevent double-finalization + if (root.closeFinalized) { + Logger.w("SmartPanel", "finalizeClose called but already finalized - ignoring", objectName); + return; + } + + // Complete the close sequence after animations finish + root.closeFinalized = true; + root.closeWatchdogActive = false; + closeWatchdogTimer.stop(); + + root.isPanelVisible = false; + root.isPanelOpen = false; + root.isClosing = false; + root.opacityFadeComplete = false; + PanelService.closedPanel(root); + closed(); + + Logger.d("SmartPanel", "Panel close finalized", objectName); } - // Expose setPosition for panels that need to recalculate on settings changes function setPosition() { - if (panelPlaceholder) { - panelPlaceholder.setPosition(); + // Don't calculate position if parent dimensions aren't available yet + // This prevents centering around (0,0) when width/height are still 0 + if (!root.width || !root.height) { + Logger.d("SmartPanel", "Skipping setPosition - dimensions not ready:", root.width, "x", root.height); + // Retry on next frame when dimensions should be available + Qt.callLater(setPosition); + return; + } + + // 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 * root.preferredWidthRatio, root.preferredWidth)); + } // Priority 3: Static preferred width + else { + w = root.preferredWidth; + } + var panelWidth = Math.min(w, root.width - 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 * root.preferredHeightRatio, root.preferredHeight)); + } // Priority 3: Static preferred height + else { + h = root.preferredHeight; + } + var panelHeight = Math.min(h, root.height - Style.barHeight - Style.marginL * 2); + + // Update panelBackground target size (will be animated) + panelBackground.targetWidth = panelWidth; + panelBackground.targetHeight = 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.allowAttach) { + // 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.allowAttach) { + 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.allowAttach && root.barIsVertical && root.barPosition === "right") { + var rightBarEdge = root.width - root.barMarginH - Style.barHeight; + calculatedX = rightBarEdge - panelWidth; + } else if (panelContent.allowAttach) { + // Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar + var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); + if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { + var rightCornerInset = Style.radiusL * 2; + calculatedX = root.width - root.barMarginH - rightCornerInset - panelWidth; + } else { + calculatedX = root.width - panelWidth; + } + } else { + calculatedX = root.width - panelWidth - Style.marginL; + } + } else if (root.effectivePanelAnchorLeft) { + if (panelContent.allowAttach && root.barIsVertical && root.barPosition === "left") { + var leftBarEdge = root.barMarginH + Style.barHeight; + calculatedX = leftBarEdge; + } else if (panelContent.allowAttach) { + // Account for corner inset when bar is floating, horizontal, AND panel is on same edge as bar + var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); + if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { + var leftCornerInset = Style.radiusL * 2; + calculatedX = root.barMarginH + leftCornerInset; + } else { + 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.allowAttach) { + 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.allowAttach && !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; + } + + // Only snap to left edge if panel is actually meant to be at left (or no explicit anchor) + var shouldSnapToLeft = root.effectivePanelAnchorLeft || (!root.hasExplicitHorizontalAnchor && root.barPosition === "left"); + // Only snap to right edge if panel is actually meant to be at right (or no explicit anchor) + var shouldSnapToRight = root.effectivePanelAnchorRight || (!root.hasExplicitHorizontalAnchor && root.barPosition === "right"); + + if (shouldSnapToLeft && Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { + calculatedX = leftEdgePos; + } else if (shouldSnapToRight && 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.allowAttach) { + calculatedY = topBarEdge; + } else { + calculatedY = topBarEdge + Style.marginM; + } + } else if (root.barPosition === "bottom") { + var bottomBarEdge = root.height - root.barMarginV - Style.barHeight; + if (panelContent.allowAttach) { + 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.allowAttach && root.barFloating) ? Style.radiusL : 0; + if (panelContent.allowAttach) { + 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.allowAttach) { + 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.allowAttach) { + calculatedY = 0; + } else { + var topBarOffset = (root.barPosition === "top") ? barOffset : 0; + calculatedY = topBarOffset + Style.marginL; + } + } else if (root.effectivePanelAnchorBottom) { + if (panelContent.allowAttach) { + 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.allowAttach) { + 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.allowAttach && !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.allowAttach && !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; + } + + // Only snap to top edge if panel is actually meant to be at top (or no explicit anchor) + var shouldSnapToTop = root.effectivePanelAnchorTop || (!root.hasExplicitVerticalAnchor && root.barPosition === "top"); + // Only snap to bottom edge if panel is actually meant to be at bottom (or no explicit anchor) + var shouldSnapToBottom = root.effectivePanelAnchorBottom || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom"); + + if (shouldSnapToTop && Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { + calculatedY = topEdgePos; + } else if (shouldSnapToBottom && Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { + calculatedY = bottomEdgePos; + } + } + + // Apply calculated positions (set targets for animation) + panelBackground.targetX = calculatedX; + panelBackground.targetY = calculatedY; + + Logger.d("SmartPanel", "Position calculated:", calculatedX, calculatedY); + Logger.d("SmartPanel", " Panel size:", panelWidth, "x", panelHeight); + Logger.d("SmartPanel", " Parent size:", root.width, "x", root.height); + } + + // 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(); + } } } - // INTERNAL IMPLEMENTATION - - // Create the panel placeholder (stays in MainScreen for background rendering) - readonly property var panelPlaceholder: PanelPlaceholder { - id: placeholder - screen: root.screen - panelName: root.objectName || "unnamed-panel" - - // Forward configuration properties - preferredWidth: root.preferredWidth - preferredHeight: root.preferredHeight - preferredWidthRatio: root.preferredWidthRatio - preferredHeightRatio: root.preferredHeightRatio - forceAttachToBar: root.forceAttachToBar - - // Forward anchoring properties - panelAnchorHorizontalCenter: root.panelAnchorHorizontalCenter - panelAnchorVerticalCenter: root.panelAnchorVerticalCenter - panelAnchorTop: root.panelAnchorTop - panelAnchorBottom: root.panelAnchorBottom - panelAnchorLeft: root.panelAnchorLeft - panelAnchorRight: root.panelAnchorRight - - // Parent to MainScreen root - parent: root.parent + // Opacity animation + // Opening: fade in after size animation reaches 75% + // Closing: fade out immediately + opacity: { + if (isClosing) + return 0.0; // Fade out when closing + if (isPanelVisible && sizeAnimationComplete) + return 1.0; // Fade in when opening + return 0.0; } - // Lazy-load the content window (only created when open, destroyed when closed) - Loader { - id: windowLoader - active: root.windowActive - sourceComponent: SmartPanelWindow { - placeholder: panelPlaceholder - panelContent: root.panelContent - panelWrapper: root // Pass reference to SmartPanel for keyboard handlers - exclusiveKeyboard: root.exclusiveKeyboard - closeWithEscape: root.closeWithEscape + Behavior on opacity { + NumberAnimation { + id: opacityAnimation + duration: root.isClosing ? Style.animationFaster : Style.animationFast + easing.type: Easing.OutQuad - // Forward signals - onPanelOpened: root.opened() - onPanelClosed: { - root.closed(); - // Destroy the window after close animation completes + onRunningChanged: { + // Safety: If animation didn't run (zero duration), handle immediately + if (!running && duration === 0) { + if (root.isClosing && root.opacity === 0.0) { + root.opacityFadeComplete = true; + var shouldFinalizeNow = panelContent.maskRegion && !panelContent.maskRegion.shouldAnimateWidth && !panelContent.maskRegion.shouldAnimateHeight; + if (shouldFinalizeNow) { + Logger.d("SmartPanel", "Zero-duration opacity + no size animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + } else if (root.isPanelVisible && root.opacity === 1.0) { + // Open completed with zero duration + root.openWatchdogActive = false; + openWatchdogTimer.stop(); + } + return; + } + + // When opacity fade completes during close, trigger size animation + if (!running && root.isClosing && root.opacity === 0.0) { + root.opacityFadeComplete = true; + // If no size animation will run (centered attached panels only), finalize immediately + // Detached panels (allowAttach === false) should always animate from top + var shouldFinalizeNow = panelContent.maskRegion && !panelContent.maskRegion.shouldAnimateWidth && !panelContent.maskRegion.shouldAnimateHeight; + if (shouldFinalizeNow) { + Logger.d("SmartPanel", "No animation - finalizing immediately", root.objectName); + Qt.callLater(root.finalizeClose); + } else { + Logger.d("SmartPanel", "Animation will run - waiting for size animation", root.objectName, "shouldAnimateHeight:", panelContent.maskRegion.shouldAnimateHeight, "shouldAnimateWidth:", panelContent.maskRegion.shouldAnimateWidth); + } + } // When opacity fade completes during open, stop watchdog + else if (!running && root.isPanelVisible && root.opacity === 1.0) { + root.openWatchdogActive = false; + openWatchdogTimer.stop(); + } + } + } + } + + // Timer to trigger opacity fade at 50% of size animation + Timer { + id: opacityTrigger + interval: Style.animationNormal * 0.5 + repeat: false + onTriggered: { + if (root.isPanelVisible) { + root.sizeAnimationComplete = true; + } + } + } + + // Watchdog timer for open sequence (safety mechanism) + Timer { + id: openWatchdogTimer + interval: Style.animationNormal * 3 // 3x normal animation time + repeat: false + onTriggered: { + if (root.openWatchdogActive) { + Logger.w("SmartPanel", "Open watchdog timeout - forcing panel visible state", root.objectName); + root.openWatchdogActive = false; + // Force completion of open sequence + if (root.isPanelOpen && !root.isPanelVisible) { + root.isPanelVisible = true; + root.sizeAnimationComplete = true; + } + } + } + } + + // Watchdog timer for close sequence (safety mechanism) + Timer { + id: closeWatchdogTimer + interval: Style.animationFast * 3 // 3x fast animation time + repeat: false + onTriggered: { + if (root.closeWatchdogActive && !root.closeFinalized) { + Logger.w("SmartPanel", "Close watchdog timeout - forcing panel close", root.objectName); + // Force finalization + Qt.callLater(root.finalizeClose); + } + } + } + + // ------------------------------------------------ + // Panel Content + Item { + id: panelContent + anchors.fill: parent + + // Screen-dependent attachment properties + readonly property bool allowAttach: Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar + readonly property bool allowAttachToBar: { + if (!(Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar) || 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: allowAttach && panelBackground.x <= 1 + readonly property bool touchingRightEdge: allowAttach && (panelBackground.x + panelBackground.width) >= (root.width - 1) + readonly property bool touchingTopEdge: allowAttach && panelBackground.y <= 1 + readonly property bool touchingBottomEdge: allowAttach && (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: allowAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.y - (root.barMarginV + Style.barHeight)) <= 1 + readonly property bool touchingBottomBar: allowAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((panelBackground.y + panelBackground.height) - (root.height - root.barMarginV - Style.barHeight)) <= 1 + readonly property bool touchingLeftBar: allowAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(panelBackground.x - (root.barMarginH + Style.barHeight)) <= 1 + readonly property bool touchingRightBar: allowAttachToBar && 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 + + // Expose self as panelItem for PanelBackground compatibility + readonly property var panelItem: panelBackground + + // Store target dimensions (set by setPosition()) + property real targetWidth: root.preferredWidth + property real targetHeight: root.preferredHeight + property real targetX: root.x + property real targetY: root.y + + property var bezierCurve: [0.05, 0, 0.133, 0.06, 0.166, 0.4, 0.208, 0.82, 0.25, 1, 1, 1] + + // Determine which edges the panel is closest to for animation direction + // Use target position (not animated position) to avoid binding loops + readonly property bool willTouchTopBar: { + if (!isPanelVisible) + return false; + if (!panelContent.allowAttachToBar || root.barPosition !== "top" || root.barIsVertical) + return false; + var targetTopBarY = root.barMarginV + Style.barHeight; + return Math.abs(panelBackground.targetY - targetTopBarY) <= 1; + } + readonly property bool willTouchBottomBar: { + if (!isPanelVisible) + return false; + if (!panelContent.allowAttachToBar || root.barPosition !== "bottom" || root.barIsVertical) + return false; + var targetBottomBarY = root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight; + return Math.abs(panelBackground.targetY - targetBottomBarY) <= 1; + } + readonly property bool willTouchLeftBar: { + if (!isPanelVisible) + return false; + if (!panelContent.allowAttachToBar || root.barPosition !== "left" || !root.barIsVertical) + return false; + var targetLeftBarX = root.barMarginH + Style.barHeight; + return Math.abs(panelBackground.targetX - targetLeftBarX) <= 1; + } + readonly property bool willTouchRightBar: { + if (!isPanelVisible) + return false; + if (!panelContent.allowAttachToBar || root.barPosition !== "right" || !root.barIsVertical) + return false; + var targetRightBarX = root.width - root.barMarginH - Style.barHeight - panelBackground.targetWidth; + return Math.abs(panelBackground.targetX - targetRightBarX) <= 1; + } + readonly property bool willTouchTopEdge: isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1 + readonly property bool willTouchBottomEdge: isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) + readonly property bool willTouchLeftEdge: isPanelVisible && panelContent.allowAttach && panelBackground.targetX <= 1 + readonly property bool willTouchRightEdge: isPanelVisible && panelContent.allowAttach && (panelBackground.targetX + panelBackground.targetWidth) >= (root.width - 1) + + readonly property bool isActuallyAttachedToAnyEdge: { + if (!isPanelVisible) + return false; + return willTouchTopBar || willTouchBottomBar || willTouchLeftBar || willTouchRightBar || willTouchTopEdge || willTouchBottomEdge || willTouchLeftEdge || willTouchRightEdge; + } + + readonly property bool animateFromTop: { + // Before panel is visible, check bar position to determine default animation + if (!isPanelVisible) { + // If bar is at top (horizontal), animate from top + // If bar is vertical (left/right), don't animate from top + return !root.barIsVertical && root.barPosition === "top"; + } + // PRIORITY 1: Bar attachment (always takes precedence) + // Attached to bar at top + if (willTouchTopBar) { + return true; + } + // PRIORITY 2: Screen edge attachment (only if not touching bar) + // Attached to screen top edge (not bar) + if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + // If panel is not attached to any edge, animate from top by default + if (!isActuallyAttachedToAnyEdge) { + return true; + } + return false; + } + readonly property bool animateFromBottom: { + if (!isPanelVisible) { + // If bar is at bottom (horizontal), animate from bottom + return !root.barIsVertical && root.barPosition === "bottom"; + } + // PRIORITY 1: Bar attachment (always takes precedence) + // Attached to bar at bottom + if (willTouchBottomBar) { + return true; + } + // PRIORITY 2: Screen edge attachment (only if not touching bar) + // Attached to screen bottom edge (not bar) + if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + return false; + } + readonly property bool animateFromLeft: { + if (!isPanelVisible) { + // If bar is at left (vertical), animate from left + return root.barIsVertical && root.barPosition === "left"; + } + // PRIORITY 1: Bar attachment (always takes precedence) + // If touching any horizontal bar, don't animate from left + if (willTouchTopBar || willTouchBottomBar) { + return false; + } + // Attached to bar at left + if (willTouchLeftBar) { + return true; + } + // PRIORITY 2: Screen edge attachment (only if not touching any bar) + // Don't animate from left if also touching top/bottom edge (priority: vertical over horizontal) + var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1; + var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1); + + if (touchingTopEdge || touchingBottomEdge) { + return false; + } + // Attached to screen left edge (not bar) + if (willTouchLeftEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { + return true; + } + return false; + } + readonly property bool animateFromRight: { + if (!isPanelVisible) { + // If bar is at right (vertical), animate from right + return root.barIsVertical && root.barPosition === "right"; + } + // PRIORITY 1: Bar attachment (always takes precedence) + // If touching any horizontal bar, don't animate from right + if (willTouchTopBar || willTouchBottomBar) { + return false; + } + // Attached to bar at right + if (willTouchRightBar) { + return true; + } + // PRIORITY 2: Screen edge attachment (only if not touching any bar) + // Don't animate from right if also touching top/bottom edge (priority: vertical over horizontal) + var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1; + var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1); + + if (touchingTopEdge || touchingBottomEdge) { + return false; + } + // Attached to screen right edge (not bar) + if (willTouchRightEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { + return true; + } + return false; + } + + // Determine animation axis based on which edge is closest + // Priority: horizontal edges (top/bottom) take precedence over vertical edges (left/right) + // This prevents diagonal animations when panel is attached to a corner + readonly property bool shouldAnimateWidth: !shouldAnimateHeight && (animateFromLeft || animateFromRight) + readonly property bool shouldAnimateHeight: animateFromTop || animateFromBottom + + // Current animated width/height (referenced by x/y for right/bottom positioning) + readonly property real currentWidth: { + // When closing and opacity fade complete, shrink width if animating horizontally + if (isClosing && opacityFadeComplete && shouldAnimateWidth) + return 0; + // When visible or closing (before opacity fade), keep full width + if (isClosing || isPanelVisible) + return targetWidth; + // Initial state before visible: + // - If we're NOT animating width, start at target (no animation needed) + // - If we ARE animating width, start at 0 (will animate to target) + return shouldAnimateWidth ? 0 : targetWidth; + } + readonly property real currentHeight: { + // When closing and opacity fade complete, shrink height if animating vertically + if (isClosing && opacityFadeComplete && shouldAnimateHeight) + return 0; + // When visible or closing (before opacity fade), keep full height + if (isClosing || isPanelVisible) + return targetHeight; + // Initial state before visible: + // - If we're NOT animating height, start at target (no animation needed) + // - If we ARE animating height, start at 0 (will animate to target) + return shouldAnimateHeight ? 0 : targetHeight; + } + + width: currentWidth + height: currentHeight + + x: { + // Offset x to make panel grow/shrink from the appropriate edge + if (animateFromRight) { + // Keep the RIGHT edge fixed at its target position + if (isPanelVisible || isClosing) { + var targetRightEdge = targetX + targetWidth; + return targetRightEdge - width; + } + } + return targetX; + } + y: { + // Offset y to make panel grow/shrink from the appropriate edge + if (animateFromBottom) { + // Keep the BOTTOM edge fixed at its target position + if (isPanelVisible || isClosing) { + var targetBottomEdge = targetY + targetHeight; + return targetBottomEdge - height; + } + } + return targetY; + } + + Behavior on width { + NumberAnimation { + id: widthAnimation + // During opening: use 0ms if not animating width, otherwise use normal duration + // During closing: use 0ms if not animating width, otherwise use fast duration + // During normal content resizing: always use normal duration + duration: (root.isOpening && !panelBackground.shouldAnimateWidth) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateWidth) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal + easing.type: Easing.BezierSpline + easing.bezierCurve: panelBackground.bezierCurve + + onRunningChanged: { + // Safety: Zero-duration animation handling + if (!running && duration === 0) { + if (root.isClosing && panelBackground.width === 0 && panelBackground.shouldAnimateWidth) { + Logger.d("SmartPanel", "Zero-duration width animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + return; + } + + // When width shrink completes during close, finalize + if (!running && root.isClosing && panelBackground.width === 0 && panelBackground.shouldAnimateWidth) { + Qt.callLater(root.finalizeClose); + } + } + } + } + + Behavior on height { + NumberAnimation { + id: heightAnimation + // During opening: use 0ms if not animating height, otherwise use normal duration + // During closing: use 0ms if not animating height, otherwise use fast duration + // During normal content resizing: always use normal duration + duration: (root.isOpening && !panelBackground.shouldAnimateHeight) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateHeight) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal + easing.type: Easing.BezierSpline + easing.bezierCurve: panelBackground.bezierCurve + + onRunningChanged: { + // Safety: Zero-duration animation handling + if (!running && duration === 0) { + if (root.isClosing && panelBackground.height === 0 && panelBackground.shouldAnimateHeight) { + Logger.d("SmartPanel", "Zero-duration height animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + return; + } + + // When height shrink completes during close, finalize + if (!running && root.isClosing && panelBackground.height === 0 && panelBackground.shouldAnimateHeight) { + Qt.callLater(root.finalizeClose); + } + } + } + } + + // 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: { + var barInverted = panelContent.allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)); + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingLeftBar; + // Invert if touching either edge that forms this corner (left OR top), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingTopEdge); + var oppositeEdgeInverted = panelContent.allowAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top"); + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingLeftEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int topRightCornerState: { + var barInverted = panelContent.allowAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)); + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingRightBar; + // Invert if touching either edge that forms this corner (right OR top), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingTopEdge); + var oppositeEdgeInverted = panelContent.allowAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top"); + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingRightEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int bottomLeftCornerState: { + var barInverted = panelContent.allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)); + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingLeftBar; + // Invert if touching either edge that forms this corner (left OR bottom), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingBottomEdge); + var oppositeEdgeInverted = panelContent.allowAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom"); + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingLeftEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int bottomRightCornerState: { + var barInverted = panelContent.allowAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)); + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingRightBar; + // Invert if touching either edge that forms this corner (right OR bottom), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingBottomEdge); + var oppositeEdgeInverted = panelContent.allowAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom"); + + if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingRightEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + 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 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + z: -1 // Behind content, but on the panel background + onClicked: mouse => { + mouse.accepted = true; // 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, calculate position then make visible + onLoaded: { + // Calculate position FIRST so targetX/targetY are set before animation starts + // This prevents the panel from animating from (0,0) on first open + setPosition(); + + // THEN make panel visible on the next frame to ensure all bindings have updated + // Qt.callLater defers execution until all current bindings are evaluated Qt.callLater(function () { - root.windowActive = false; + root.isPanelVisible = true; + opacityTrigger.start(); + + // Start open watchdog timer + root.openWatchdogActive = true; + openWatchdogTimer.start(); + + opened(); }); } } } - // Register with PanelService Component.onCompleted: { PanelService.registerPanel(root); } diff --git a/Modules/MainScreen/SmartPanelWindow.qml b/Modules/MainScreen/SmartPanelWindow.qml deleted file mode 100644 index de47ff29..00000000 --- a/Modules/MainScreen/SmartPanelWindow.qml +++ /dev/null @@ -1,454 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services.Compositor -import qs.Services.UI - -/** -* SmartPanelWindow - Separate window for panel content -* -* This component runs in its own window, separate from MainScreen. -* It follows the PanelPlaceholder for positioning and contains the actual panel content. -*/ -PanelWindow { - id: root - - // Required reference to placeholder - required property PanelPlaceholder placeholder - - // Panel content component (set by SmartPanel wrapper) - property Component panelContent: null - - // Reference to the SmartPanel wrapper (for keyboard handlers) - property var panelWrapper: null - - // Keyboard focus - property bool exclusiveKeyboard: true - - // Support close with escape - property bool closeWithEscape: true - - // Track whether panel is open - property bool isPanelOpen: false - - // Track actual visibility (delayed until content is loaded and sized) - property bool isPanelVisible: false - - // Track size animation completion for sequential opacity animation - property bool sizeAnimationComplete: false - - // Track close animation state - property bool isClosing: false - property bool opacityFadeComplete: false - property bool closeFinalized: false - - // Safety: Watchdog timers - property bool closeWatchdogActive: false - property bool openWatchdogActive: false - - // Signals - signal panelOpened - signal panelClosed - - // Window configuration - color: Color.transparent - mask: null // No mask - content window is rectangular - visible: isPanelOpen - screen: placeholder.screen // Explicitly set screen to match placeholder - - // Wayland layer shell configuration - fullscreen window - WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "noctalia-panel-content-" + placeholder.panelName + "-" + (placeholder.screen?.name || "unknown") - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: { - if (!root.isPanelOpen) { - return WlrKeyboardFocus.None; - } - if (CompositorService.isHyprland) { - // Exclusive focus on hyprland is too restrictive. - return WlrKeyboardFocus.OnDemand; - } else { - return root.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand; - } - } - - // Anchor to all edges to make fullscreen - anchors { - top: true - bottom: true - left: true - right: true - } - - // Margins to exclude bar area so bar remains clickable - margins { - top: placeholder.barPosition === "top" ? (placeholder.barMarginV + Style.barHeight) : 0 - bottom: placeholder.barPosition === "bottom" ? (placeholder.barMarginV + Style.barHeight) : 0 - left: placeholder.barPosition === "left" ? (placeholder.barMarginH + Style.barHeight) : 0 - right: placeholder.barPosition === "right" ? (placeholder.barMarginH + Style.barHeight) : 0 - } - - // Sync state to placeholder - onIsPanelVisibleChanged: { - placeholder.isPanelVisible = isPanelVisible; - } - onIsClosingChanged: { - placeholder.isClosing = isClosing; - } - onOpacityFadeCompleteChanged: { - placeholder.opacityFadeComplete = opacityFadeComplete; - } - onSizeAnimationCompleteChanged: { - placeholder.sizeAnimationComplete = sizeAnimationComplete; - } - - // 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, placeholder.screen.name); - } - - if (buttonItem) { - placeholder.buttonItem = buttonItem; - // Map button position to screen coordinates - var buttonPos = buttonItem.mapToItem(null, 0, 0); - placeholder.buttonPosition = Qt.point(buttonPos.x, buttonPos.y); - placeholder.buttonWidth = buttonItem.width; - placeholder.buttonHeight = buttonItem.height; - placeholder.useButtonPosition = true; - } else { - // No button provided: reset button position mode - placeholder.buttonItem = null; - placeholder.useButtonPosition = false; - } - - // Set isPanelOpen to trigger content loading - isPanelOpen = true; - - // Notify PanelService - PanelService.willOpenPanel(root); - } - - function close() { - // Start close sequence: fade opacity first - isClosing = true; - sizeAnimationComplete = false; - closeFinalized = false; - - // Stop the open animation timer if it's still running - opacityTrigger.stop(); - openWatchdogActive = false; - openWatchdogTimer.stop(); - - // Start close watchdog timer - closeWatchdogActive = true; - closeWatchdogTimer.restart(); - - // If opacity is already 0, skip directly to size animation - if (contentWrapper.opacity === 0.0) { - opacityFadeComplete = true; - } else { - opacityFadeComplete = false; - } - - Logger.d("SmartPanelWindow", "Closing panel", placeholder.panelName); - } - - function finalizeClose() { - // Prevent double-finalization - if (root.closeFinalized) { - Logger.w("SmartPanelWindow", "finalizeClose called but already finalized - ignoring", placeholder.panelName); - return; - } - - // Complete the close sequence after animations finish - root.closeFinalized = true; - root.closeWatchdogActive = false; - closeWatchdogTimer.stop(); - - root.isPanelVisible = false; - root.isPanelOpen = false; - root.isClosing = false; - root.opacityFadeComplete = false; - PanelService.closedPanel(root); - panelClosed(); - - Logger.d("SmartPanelWindow", "Panel close finalized", placeholder.panelName); - } - - // Fullscreen container for click-to-close and content - Item { - anchors.fill: parent - focus: true // Enable keyboard event handling - - // Handle keyboard events directly via Keys handler - Keys.onPressed: event => { - Logger.d("SmartPanelWindow", "Key pressed:", event.key, "for panel:", placeholder.panelName); - if (event.key === Qt.Key_Escape) { - panelWrapper.onEscapePressed(); - if (closeWithEscape) { - root.close(); - event.accepted = true; - } - } else if (panelWrapper) { - if (event.key === Qt.Key_Up && panelWrapper.onUpPressed) { - panelWrapper.onUpPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Down && panelWrapper.onDownPressed) { - panelWrapper.onDownPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Left && panelWrapper.onLeftPressed) { - panelWrapper.onLeftPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Right && panelWrapper.onRightPressed) { - panelWrapper.onRightPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Tab && panelWrapper.onTabPressed) { - panelWrapper.onTabPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Backtab && panelWrapper.onBackTabPressed) { - panelWrapper.onBackTabPressed(); - event.accepted = true; - } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && panelWrapper.onReturnPressed) { - panelWrapper.onReturnPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Home && panelWrapper.onHomePressed) { - panelWrapper.onHomePressed(); - event.accepted = true; - } else if (event.key === Qt.Key_End && panelWrapper.onEndPressed) { - panelWrapper.onEndPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_PageUp && panelWrapper.onPageUpPressed) { - panelWrapper.onPageUpPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_PageDown && panelWrapper.onPageDownPressed) { - panelWrapper.onPageDownPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlJPressed) { - panelWrapper.onCtrlJPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlKPressed) { - panelWrapper.onCtrlKPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlNPressed) { - panelWrapper.onCtrlNPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_P && (event.modifiers & Qt.ControlModifier) && panelWrapper.onCtrlPPressed) { - panelWrapper.onCtrlPPressed(); - event.accepted = true; - } - } - } - - // Background MouseArea for click-to-close (behind content) - MouseArea { - anchors.fill: parent - enabled: root.isPanelOpen && !root.isClosing - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - root.close(); - mouse.accepted = true; - } - z: 0 - } - - // Content wrapper with opacity animation - Item { - id: contentWrapper - // Position at placeholder location, compensating for window margins - x: placeholder.panelItem.x - (placeholder.barPosition === "left" ? (placeholder.barMarginH + Style.barHeight) : 0) - y: placeholder.panelItem.y - (placeholder.barPosition === "top" ? (placeholder.barMarginV + Style.barHeight) : 0) - width: placeholder.panelItem.width - height: placeholder.panelItem.height - z: 1 // Above click-to-close MouseArea - - // Opacity animation - opacity: { - if (isClosing) - return 0.0; - if (isPanelVisible && sizeAnimationComplete) - return 1.0; - return 0.0; - } - - Behavior on opacity { - NumberAnimation { - id: opacityAnimation - duration: root.isClosing ? Style.animationFaster : Style.animationFast - easing.type: Easing.OutQuad - - onRunningChanged: { - // Safety: Zero-duration animation handling - if (!running && duration === 0) { - if (root.isClosing && contentWrapper.opacity === 0.0) { - root.opacityFadeComplete = true; - var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight; - if (shouldFinalizeNow) { - Logger.d("SmartPanelWindow", "Zero-duration opacity + no size animation - finalizing", placeholder.panelName); - Qt.callLater(root.finalizeClose); - } - } else if (root.isPanelVisible && contentWrapper.opacity === 1.0) { - root.openWatchdogActive = false; - openWatchdogTimer.stop(); - } - return; - } - - // When opacity fade completes during close, trigger size animation - if (!running && root.isClosing && contentWrapper.opacity === 0.0) { - root.opacityFadeComplete = true; - var shouldFinalizeNow = placeholder.panelItem && !placeholder.panelItem.shouldAnimateWidth && !placeholder.panelItem.shouldAnimateHeight; - if (shouldFinalizeNow) { - Logger.d("SmartPanelWindow", "No animation - finalizing immediately", placeholder.panelName); - Qt.callLater(root.finalizeClose); - } else { - Logger.d("SmartPanelWindow", "Animation will run - waiting for size animation", placeholder.panelName); - } - } // When opacity fade completes during open, stop watchdog - else if (!running && root.isPanelVisible && contentWrapper.opacity === 1.0) { - root.openWatchdogActive = false; - openWatchdogTimer.stop(); - } - } - } - } - - // Panel content loader - Loader { - id: contentLoader - active: isPanelOpen - anchors.fill: parent - sourceComponent: root.panelContent - - // When content finishes loading, trigger positioning and visibility - onLoaded: { - // Capture initial content-driven size if available - if (contentLoader.item) { - var hasWidthProp = contentLoader.item.hasOwnProperty('contentPreferredWidth'); - var hasHeightProp = contentLoader.item.hasOwnProperty('contentPreferredHeight'); - - if (hasWidthProp || hasHeightProp) { - var initialWidth = hasWidthProp ? contentLoader.item.contentPreferredWidth : 0; - var initialHeight = hasHeightProp ? contentLoader.item.contentPreferredHeight : 0; - placeholder.updateContentSize(initialWidth, initialHeight); - Logger.d("SmartPanelWindow", "Initial content size:", initialWidth, "x", initialHeight, placeholder.panelName); - } - } - - // Calculate position in placeholder - placeholder.setPosition(); - - // Make panel visible on the next frame - Qt.callLater(function () { - root.isPanelVisible = true; - opacityTrigger.start(); - - // Start open watchdog timer - root.openWatchdogActive = true; - openWatchdogTimer.start(); - - panelOpened(); - }); - } - } - - // MouseArea to prevent clicks on panel content from closing it - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - mouse.accepted = true; // Eat the click to prevent propagation to background - } - z: -1 // Behind content but above background click-to-close - } - - // Watch for changes in content-driven sizes - Connections { - target: contentLoader.item - ignoreUnknownSignals: true - - function onContentPreferredWidthChanged() { - if (root.isPanelOpen && root.isPanelVisible && contentLoader.item) { - placeholder.updateContentSize(contentLoader.item.contentPreferredWidth, placeholder.contentPreferredHeight); - } - } - - function onContentPreferredHeightChanged() { - if (root.isPanelOpen && root.isPanelVisible && contentLoader.item) { - placeholder.updateContentSize(placeholder.contentPreferredWidth, contentLoader.item.contentPreferredHeight); - } - } - } - } - } - - // Timer to trigger opacity fade at 50% of size animation - Timer { - id: opacityTrigger - interval: Style.animationNormal * 0.5 - repeat: false - onTriggered: { - if (root.isPanelVisible) { - root.sizeAnimationComplete = true; - } - } - } - - // Watchdog timer for open sequence - Timer { - id: openWatchdogTimer - interval: Style.animationNormal * 3 - repeat: false - onTriggered: { - if (root.openWatchdogActive) { - Logger.w("SmartPanelWindow", "Open watchdog timeout - forcing panel visible state", placeholder.panelName); - root.openWatchdogActive = false; - if (root.isPanelOpen && !root.isPanelVisible) { - root.isPanelVisible = true; - root.sizeAnimationComplete = true; - } - } - } - } - - // Watchdog timer for close sequence - Timer { - id: closeWatchdogTimer - interval: Style.animationFast * 3 - repeat: false - onTriggered: { - if (root.closeWatchdogActive && !root.closeFinalized) { - Logger.w("SmartPanelWindow", "Close watchdog timeout - forcing panel close", placeholder.panelName); - Qt.callLater(root.finalizeClose); - } - } - } - - // Watch for placeholder size animation completion to finalize close - Connections { - target: placeholder.panelItem - - function onWidthChanged() { - // When width shrinks to 0 during close and we're animating width, finalize - if (root.isClosing && placeholder.panelItem.width === 0 && placeholder.panelItem.shouldAnimateWidth) { - Qt.callLater(root.finalizeClose); - } - } - - function onHeightChanged() { - // When height shrinks to 0 during close and we're animating height, finalize - if (root.isClosing && placeholder.panelItem.height === 0 && placeholder.panelItem.shouldAnimateHeight) { - Qt.callLater(root.finalizeClose); - } - } - } -} diff --git a/Services/Media/CavaService.qml b/Services/Media/CavaService.qml index 1996b251..08fd04b2 100644 --- a/Services/Media/CavaService.qml +++ b/Services/Media/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.panelWrapper.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: 32