diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 94b3dfe2..da203856 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -213,7 +213,8 @@ "respectExpireTimeout": false, "lowUrgencyDuration": 3, "normalUrgencyDuration": 8, - "criticalUrgencyDuration": 15 + "criticalUrgencyDuration": 15, + "enableKeyboardLayoutToast": true }, "osd": { "enabled": true, diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml index 0043bcd6..7da89294 100644 --- a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -67,91 +67,91 @@ Item { // Audio PanelBackground { - panel: root.windowRoot.audioPanel + panel: root.windowRoot.audioPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Battery PanelBackground { - panel: root.windowRoot.batteryPanel + panel: root.windowRoot.batteryPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Bluetooth PanelBackground { - panel: root.windowRoot.bluetoothPanel + panel: root.windowRoot.bluetoothPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Calendar PanelBackground { - panel: root.windowRoot.calendarPanel + panel: root.windowRoot.calendarPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Control Center PanelBackground { - panel: root.windowRoot.controlCenterPanel + panel: root.windowRoot.controlCenterPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Launcher PanelBackground { - panel: root.windowRoot.launcherPanel + panel: root.windowRoot.launcherPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) } // Notification History PanelBackground { - panel: root.windowRoot.notificationHistoryPanel + panel: root.windowRoot.notificationHistoryPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Session Menu PanelBackground { - panel: root.windowRoot.sessionMenuPanel + panel: root.windowRoot.sessionMenuPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Settings PanelBackground { - panel: root.windowRoot.settingsPanel + panel: root.windowRoot.settingsPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Setup Wizard PanelBackground { - panel: root.windowRoot.setupWizardPanel + panel: root.windowRoot.setupWizardPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // TrayDrawer PanelBackground { - panel: root.windowRoot.trayDrawerPanel + panel: root.windowRoot.trayDrawerPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // Wallpaper PanelBackground { - panel: root.windowRoot.wallpaperPanel + panel: root.windowRoot.wallpaperPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } // WiFi PanelBackground { - panel: root.windowRoot.wifiPanel + panel: root.windowRoot.wifiPanelPlaceholder shapeContainer: backgroundsShape backgroundColor: Color.mSurface } diff --git a/Modules/MainScreen/Backgrounds/PanelBackground.qml b/Modules/MainScreen/Backgrounds/PanelBackground.qml index e91bface..82ce35ad 100644 --- a/Modules/MainScreen/Backgrounds/PanelBackground.qml +++ b/Modules/MainScreen/Backgrounds/PanelBackground.qml @@ -10,6 +10,9 @@ import qs.Modules.MainScreen.Backgrounds * Unified shadow system. This component is a ShapePath that will * be a child of the unified AllBackgrounds Shape container. * + * Reads positioning and geometry from PanelPlaceholder (via panel.panelItem). + * The actual panel content lives in a separate SmartPanelWindow. + * * Uses 4-state per-corner system for flexible corner rendering: * - State -1: No radius (flat/square corner) * - State 0: Normal (inner curve) @@ -19,7 +22,7 @@ import qs.Modules.MainScreen.Backgrounds ShapePath { id: root - // Required reference to the panel component (e.g., windowRoot.controlCenterPanel) + // Required reference to PanelPlaceholder (e.g., windowRoot.audioPanelPlaceholder) required property var panel // Required reference to AllBackgrounds shapeContainer @@ -30,9 +33,9 @@ ShapePath { required property color backgroundColor - // Get the actual panelBackground Item from SmartPanel - // Only access panelRegion if panel exists and is visible - readonly property var panelBg: (panel && panel.visible) ? panel.panelRegion : null + // Get the actual panelBackground Item from PanelPlaceholder + // Only access panelItem if panel exists and is visible + readonly property var panelBg: (panel && panel.visible) ? panel.panelItem : null // Panel position - panelBg is in screen coordinates already readonly property real panelX: panelBg ? panelBg.x : 0 diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 3187ebc7..2c15f406 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -4,7 +4,6 @@ import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services.Compositor import qs.Services.UI import "Backgrounds" as Backgrounds @@ -47,6 +46,21 @@ 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 calendarPanelPlaceholder: calendarPanel.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 + Component.onCompleted: { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) } @@ -55,17 +69,7 @@ PanelWindow { WlrLayershell.layer: WlrLayer.Top WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that - - // Different compositor handle the keyboard focus differently (inc. mouse) - // This ensures all keyboard shortcuts work reliably (Escape, etc.) - // Also ensures that the launcher get proper focus on launch. - WlrLayershell.keyboardFocus: { - if (CompositorService.isNiri) { - return root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - } else { - return root.isPanelOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - } - } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { top: true @@ -437,178 +441,4 @@ PanelWindow { */ ScreenCorners {} } - - // Centralized keyboard shortcuts - delegate to opened panel - // This ensures shortcuts work regardless of panel focus state - Shortcut { - sequence: "Escape" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) { - PanelService.openedPanel.onEscapePressed() - } else if (PanelService.openedPanel) { - PanelService.openedPanel.close() - } - } - } - - Shortcut { - sequence: "Tab" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) { - PanelService.openedPanel.onTabPressed() - } - } - } - - Shortcut { - sequence: "Shift+Tab" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) { - PanelService.openedPanel.onShiftTabPressed() - } - } - } - - Shortcut { - sequence: "Up" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) { - PanelService.openedPanel.onUpPressed() - } - } - } - - Shortcut { - sequence: "Down" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) { - PanelService.openedPanel.onDownPressed() - } - } - } - - Shortcut { - sequence: "Return" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { - PanelService.openedPanel.onReturnPressed() - } - } - } - - Shortcut { - sequence: "Enter" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { - PanelService.openedPanel.onReturnPressed() - } - } - } - - Shortcut { - sequence: "Home" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) { - PanelService.openedPanel.onHomePressed() - } - } - } - - Shortcut { - sequence: "End" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) { - PanelService.openedPanel.onEndPressed() - } - } - } - - Shortcut { - sequence: "PgUp" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) { - PanelService.openedPanel.onPageUpPressed() - } - } - } - - Shortcut { - sequence: "PgDown" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) { - PanelService.openedPanel.onPageDownPressed() - } - } - } - - Shortcut { - sequence: "Ctrl+J" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) { - PanelService.openedPanel.onCtrlJPressed() - } - } - } - - Shortcut { - sequence: "Ctrl+K" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) { - PanelService.openedPanel.onCtrlKPressed() - } - } - } - - Shortcut { - sequence: "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() - } - } - } - - Shortcut { - sequence: "Left" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) { - PanelService.openedPanel.onLeftPressed() - } - } - } - - Shortcut { - sequence: "Right" - enabled: root.isPanelOpen - onActivated: { - if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) { - PanelService.openedPanel.onRightPressed() - } - } - } } diff --git a/Modules/MainScreen/PanelPlaceholder.qml b/Modules/MainScreen/PanelPlaceholder.qml new file mode 100644 index 00000000..af50d4c4 --- /dev/null +++ b/Modules/MainScreen/PanelPlaceholder.qml @@ -0,0 +1,702 @@ +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 + + // 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 + + // 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 { + duration: { + if (!panelBackground.shouldAnimateWidth) + return 0 + return root.isClosing ? Style.animationFast : Style.animationNormal + } + easing.type: Easing.BezierSpline + easing.bezierCurve: panelBackground.bezierCurve + } + } + + Behavior on height { + NumberAnimation { + duration: { + if (!panelBackground.shouldAnimateHeight) + return 0 + return 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 58cabc37..a9a335ca 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -5,11 +5,18 @@ import qs.Services.UI /** - * SmartPanel for use within MainScreen + * 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) */ Item { id: root + // PUBLIC API - unchanged for backward compatibility + // Screen property provided by MainScreen property ShellScreen screen: null @@ -34,1056 +41,173 @@ Item { property bool panelAnchorLeft: false property bool panelAnchorRight: false - // Button position properties - property bool useButtonPosition: false - property point buttonPosition: Qt.point(0, 0) - property int buttonWidth: 0 - property int buttonHeight: 0 + // Track if window has been created (for lazy loading) + property bool windowCreated: false - // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap - property real edgeSnapDistance: 50 + // 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 - // 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: 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 - - // Keyboard event handlers - override these in specific panels to handle shortcuts - // These are called from MainScreen's centralized shortcuts - function onEscapePressed() { - close() - } - function onTabPressed() {} - function onShiftTabPressed() {} - function onUpPressed() {} - function onDownPressed() {} - function onLeftPressed() {} - function onRightPressed() {} - function onReturnPressed() {} - function onHomePressed() {} - function onEndPressed() {} - function onPageUpPressed() {} - function onPageDownPressed() {} - function onCtrlJPressed() {} - function onCtrlKPressed() {} - - // Expose panel region for click-through mask - readonly property var panelRegion: panelContent.maskRegion - - readonly property string barPosition: Settings.data.bar.position - readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" - readonly property bool barFloating: Settings.data.bar.floating - readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - - // Helper to detect if any anchor is explicitly set - readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight - readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom - - // Effective anchor properties (depend on 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) + // Expose panelRegion for backward compatibility (MainScreen mask) + readonly property var panelRegion: panelPlaceholder ? panelPlaceholder.panelItem : null + // Signals signal opened signal closed - Connections { - target: Style - - function onUiScaleRatioChanged() { - if (root.isPanelOpen && root.isPanelVisible) { - root.setPosition() - } - } + // Keyboard event handlers - these can be overridden by panel implementations + function onEscapePressed() { + if (windowLoader.item) + windowLoader.item.handleEscapePressed() + } + function onTabPressed() { + if (windowLoader.item) + windowLoader.item.handleTabPressed() + } + function onShiftTabPressed() { + if (windowLoader.item) + windowLoader.item.handleShiftTabPressed() + } + function onUpPressed() { + if (windowLoader.item) + windowLoader.item.handleUpPressed() + } + function onDownPressed() { + if (windowLoader.item) + windowLoader.item.handleDownPressed() + } + function onLeftPressed() { + if (windowLoader.item) + windowLoader.item.handleLeftPressed() + } + function onRightPressed() { + if (windowLoader.item) + windowLoader.item.handleRightPressed() + } + function onReturnPressed() { + if (windowLoader.item) + windowLoader.item.handleReturnPressed() + } + function onHomePressed() { + if (windowLoader.item) + windowLoader.item.handleHomePressed() + } + function onEndPressed() { + if (windowLoader.item) + windowLoader.item.handleEndPressed() + } + function onPageUpPressed() { + if (windowLoader.item) + windowLoader.item.handlePageUpPressed() + } + function onPageDownPressed() { + if (windowLoader.item) + windowLoader.item.handlePageDownPressed() + } + function onCtrlJPressed() { + if (windowLoader.item) + windowLoader.item.handleCtrlJPressed() + } + function onCtrlKPressed() { + if (windowLoader.item) + windowLoader.item.handleCtrlKPressed() } - // Panel visibility and sizing - visible: isPanelVisible - width: parent ? parent.width : 0 - height: parent ? parent.height : 0 - - // Panel control functions + // Public control functions function toggle(buttonItem, buttonName) { - if (!isPanelOpen) { - open(buttonItem, buttonName) - } else { - close() + // Ensure window is created before toggling + if (!windowCreated) { + windowCreated = true + Qt.callLater(function () { + if (windowLoader.item) { + windowLoader.item.toggle(buttonItem, buttonName) + } + }) + } else if (windowLoader.item) { + windowLoader.item.toggle(buttonItem, buttonName) } } function open(buttonItem, buttonName) { - if (!buttonItem && buttonName) { - buttonItem = BarService.lookupWidget(buttonName, screen.name) + // Ensure window is created before opening + if (!windowCreated) { + windowCreated = true + Qt.callLater(function () { + if (windowLoader.item) { + windowLoader.item.open(buttonItem, buttonName) + } + }) + } else if (windowLoader.item) { + windowLoader.item.open(buttonItem, buttonName) } - - 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() { - // 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 + if (windowLoader.item) { + windowLoader.item.close() } - - // 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() { - // 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() - } + if (panelPlaceholder) { + panelPlaceholder.setPosition() } } - // 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 + // 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 } - Behavior on opacity { - NumberAnimation { - id: opacityAnimation - duration: root.isClosing ? Style.animationFaster : Style.animationFast - easing.type: Easing.OutQuad + // Lazy-load the content window (only created on first open) + Loader { + id: windowLoader + active: root.windowCreated + sourceComponent: SmartPanelWindow { + placeholder: panelPlaceholder + panelContent: root.panelContent + panelWrapper: root // Pass reference to SmartPanel for keyboard handlers + panelBackgroundColor: root.panelBackgroundColor + panelBorderColor: root.panelBorderColor - 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() - } - } + // Forward signals + onPanelOpened: root.opened() + onPanelClosed: root.closed() } } - // 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 + // Register with PanelService (backward compatibility) + // Note: Registration happens in MainScreen after objectName is set + Component.onCompleted: { + // Use Qt.callLater to ensure objectName is set by parent before registering + Qt.callLater(function () { + if (!objectName) { + Logger.w("SmartPanel", "Panel created without objectName - PanelService registration may fail") } - } - } - - // 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 - - // 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, default to top animation to avoid diagonal animation on first open - if (!isPanelVisible) { - return true - } - // 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) { - return false - } - // 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) { - return false - } - // 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) { - return false - } - // 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: always start at 0 for width animation - // (will only animate if shouldAnimateWidth becomes true when isPanelVisible changes) - return 0 - } - 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: always start at 0 for height animation - // (will only animate if shouldAnimateHeight becomes true when isPanelVisible changes) - return 0 - } - - 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 - duration: { - if (!panelBackground.shouldAnimateWidth) - return 0 - return 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 - duration: { - if (!panelBackground.shouldAnimateHeight) - return 0 - return 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.isPanelVisible = true - opacityTrigger.start() - - // Start open watchdog timer - root.openWatchdogActive = true - openWatchdogTimer.start() - - opened() - }) - } - } + PanelService.registerPanel(root) + }) } } diff --git a/Modules/MainScreen/SmartPanelWindow.qml b/Modules/MainScreen/SmartPanelWindow.qml new file mode 100644 index 00000000..bea07ef9 --- /dev/null +++ b/Modules/MainScreen/SmartPanelWindow.qml @@ -0,0 +1,514 @@ +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 + + // Color properties (forwarded from SmartPanel) + property color panelBackgroundColor: Color.mSurface + property color panelBorderColor: Color.mOutline + + // 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 + + // Keyboard event handlers (forwarded from SmartPanel wrapper) + function handleEscapePressed() { + close() + } + function handleTabPressed() { + if (contentLoader.item && contentLoader.item.onTabPressed) { + contentLoader.item.onTabPressed() + } + } + function handleShiftTabPressed() { + if (contentLoader.item && contentLoader.item.onShiftTabPressed) { + contentLoader.item.onShiftTabPressed() + } + } + function handleUpPressed() { + if (contentLoader.item && contentLoader.item.onUpPressed) { + contentLoader.item.onUpPressed() + } + } + function handleDownPressed() { + if (contentLoader.item && contentLoader.item.onDownPressed) { + contentLoader.item.onDownPressed() + } + } + function handleLeftPressed() { + if (contentLoader.item && contentLoader.item.onLeftPressed) { + contentLoader.item.onLeftPressed() + } + } + function handleRightPressed() { + if (contentLoader.item && contentLoader.item.onRightPressed) { + contentLoader.item.onRightPressed() + } + } + function handleReturnPressed() { + if (contentLoader.item && contentLoader.item.onReturnPressed) { + contentLoader.item.onReturnPressed() + } + } + function handleHomePressed() { + if (contentLoader.item && contentLoader.item.onHomePressed) { + contentLoader.item.onHomePressed() + } + } + function handleEndPressed() { + if (contentLoader.item && contentLoader.item.onEndPressed) { + contentLoader.item.onEndPressed() + } + } + function handlePageUpPressed() { + if (contentLoader.item && contentLoader.item.onPageUpPressed) { + contentLoader.item.onPageUpPressed() + } + } + function handlePageDownPressed() { + if (contentLoader.item && contentLoader.item.onPageDownPressed) { + contentLoader.item.onPageDownPressed() + } + } + function handleCtrlJPressed() { + if (contentLoader.item && contentLoader.item.onCtrlJPressed) { + contentLoader.item.onCtrlJPressed() + } + } + function handleCtrlKPressed() { + if (contentLoader.item && contentLoader.item.onCtrlKPressed) { + contentLoader.item.onCtrlKPressed() + } + } + + // Window configuration + color: "transparent" + mask: null // No mask - content window is rectangular + visible: isPanelOpen + + // 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 + + // Different compositor handle the keyboard focus differently (inc. mouse) + // This ensures all keyboard shortcuts work reliably (Escape, etc.) + // Also ensures that the launcher get proper focus on launch. + WlrLayershell.keyboardFocus: { + if (CompositorService.isNiri) { + return root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + } else { + return root.isPanelOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + } + } + + // Anchor to all edges to make fullscreen + anchors { + top: true + bottom: true + left: true + right: true + } + + // Sync state to placeholder + onIsPanelVisibleChanged: { + placeholder.isPanelVisible = isPanelVisible + } + onIsClosingChanged: { + placeholder.isClosing = isClosing + } + onOpacityFadeCompleteChanged: { + placeholder.opacityFadeComplete = opacityFadeComplete + } + + // 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) { + Logger.d("SmartPanelWindow", "Escape - closing panel") + root.close() + event.accepted = true + } else if (panelWrapper) { + // Forward to panelWrapper's onXXXPressed methods (defined in SmartPanel/Launcher) + if (event.key === Qt.Key_Up && panelWrapper.onUpPressed) { + Logger.d("SmartPanelWindow", "Up - forwarding to panelWrapper") + panelWrapper.onUpPressed() + event.accepted = true + } else if (event.key === Qt.Key_Down && panelWrapper.onDownPressed) { + Logger.d("SmartPanelWindow", "Down - forwarding to panelWrapper") + 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) { + if (event.modifiers & Qt.ShiftModifier && panelWrapper.onShiftTabPressed) { + panelWrapper.onShiftTabPressed() + } else if (panelWrapper.onTabPressed) { + panelWrapper.onTabPressed() + } + 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 within fullscreen window + x: placeholder.panelItem.x + y: placeholder.panelItem.y + 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) + } + } + } +}