mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-05-24 07:09:51 +00:00
851 lines
41 KiB
QML
851 lines
41 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Commons
|
|
import qs.Services
|
|
|
|
|
|
/**
|
|
* NPanel for use within NFullScreenWindow
|
|
*/
|
|
Item {
|
|
id: root
|
|
|
|
// Screen property provided by NFullScreenWindow
|
|
property ShellScreen screen: null
|
|
|
|
// Get bar configuration for this screen
|
|
property var barConfig: screen ? Settings.getMonitorBarConfig(screen.name) : Settings.getDefaultBarConfig()
|
|
readonly property real barHeight: BarService.getBarHeight(barConfig.density, barConfig.position)
|
|
|
|
// Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap
|
|
property real edgeSnapDistance: 50
|
|
|
|
property Component panelContent: null
|
|
|
|
// Panel size properties
|
|
property real preferredWidth: 700
|
|
property real preferredHeight: 900
|
|
property real preferredWidthRatio
|
|
property real preferredHeightRatio
|
|
property color panelBackgroundColor: Color.mSurface
|
|
property color panelBorderColor: Color.mOutline
|
|
property var buttonItem: null
|
|
|
|
// Anchoring properties
|
|
property bool panelAnchorHorizontalCenter: false
|
|
property bool panelAnchorVerticalCenter: false
|
|
property bool panelAnchorTop: false
|
|
property bool panelAnchorBottom: false
|
|
property bool panelAnchorLeft: false
|
|
property bool panelAnchorRight: false
|
|
|
|
// Button position properties
|
|
property bool useButtonPosition: false
|
|
property point buttonPosition: Qt.point(0, 0)
|
|
property int buttonWidth: 0
|
|
property int buttonHeight: 0
|
|
|
|
// Track whether panel is open
|
|
property bool isPanelOpen: false
|
|
|
|
// Animation properties
|
|
property real animationProgress: 0
|
|
property bool isClosing: false
|
|
|
|
// Keyboard event handlers - override these in specific panels to handle shortcuts
|
|
// These are called from NFullScreenWindow's centralized shortcuts
|
|
function onEscapePressed() {
|
|
close()
|
|
}
|
|
function onTabPressed() {}
|
|
function onShiftTabPressed() {}
|
|
function onUpPressed() {}
|
|
function onDownPressed() {}
|
|
function onLeftPressed() {}
|
|
function onRightPressed() {}
|
|
function onReturnPressed() {}
|
|
function onHomePressed() {}
|
|
function onEndPressed() {}
|
|
function onPageUpPressed() {}
|
|
function onPageDownPressed() {}
|
|
function onCtrlJPressed() {}
|
|
function onCtrlKPressed() {}
|
|
|
|
Behavior on animationProgress {
|
|
NumberAnimation {
|
|
duration: Style.animationNormal
|
|
easing.type: Easing.OutQuint
|
|
onRunningChanged: {
|
|
// When close animation finishes, actually hide the panel
|
|
if (!running && root.isClosing) {
|
|
root.isClosing = false
|
|
root.isPanelOpen = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expose panel region for click-through mask (only when open)
|
|
readonly property var panelRegion: panelContentContainer.item?.maskRegion || null
|
|
|
|
readonly property string barPosition: root.barConfig.position || "top"
|
|
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
|
readonly property bool barFloating: root.barConfig.floating || false
|
|
readonly property real barMarginH: barFloating ? root.barConfig.marginHorizontal * Style.marginXL : 0
|
|
readonly property real barMarginV: barFloating ? root.barConfig.marginVertical * Style.marginXL : 0
|
|
|
|
// Helper to detect if any anchor is explicitly set
|
|
readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight
|
|
readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom
|
|
|
|
signal opened
|
|
signal closed
|
|
|
|
// Panel visibility and sizing
|
|
// Keep visible during close animation
|
|
visible: isPanelOpen || isClosing
|
|
width: parent ? parent.width : 0
|
|
height: parent ? parent.height : 0
|
|
|
|
// Panel control functions
|
|
function toggle(buttonItem, buttonName) {
|
|
if (!isPanelOpen) {
|
|
open(buttonItem, buttonName)
|
|
} else {
|
|
close()
|
|
}
|
|
}
|
|
|
|
function open(buttonItem, buttonName) {
|
|
if (!buttonItem && buttonName) {
|
|
buttonItem = BarService.lookupWidget(buttonName, screen.name)
|
|
}
|
|
|
|
if (buttonItem) {
|
|
root.buttonItem = buttonItem
|
|
// Map button position to screen coordinates
|
|
var buttonPos = buttonItem.mapToItem(null, 0, 0)
|
|
root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y)
|
|
root.buttonWidth = buttonItem.width
|
|
root.buttonHeight = buttonItem.height
|
|
root.useButtonPosition = true
|
|
} else {
|
|
// No button provided: reset button position mode
|
|
root.buttonItem = null
|
|
root.useButtonPosition = false
|
|
}
|
|
|
|
setPosition()
|
|
isPanelOpen = true
|
|
animationProgress = 1
|
|
|
|
// Notify PanelService
|
|
PanelService.willOpenPanel(root)
|
|
|
|
// Delay the opened signal to ensure content is fully loaded
|
|
// This ensures Component.onCompleted of the loaded content runs first
|
|
Qt.callLater(() => {
|
|
opened()
|
|
})
|
|
|
|
Logger.d("NPanel", "Opened panel", objectName)
|
|
Logger.d("NPanel", " Root size:", width, "x", height)
|
|
}
|
|
|
|
function close() {
|
|
// Start close animation
|
|
isClosing = true
|
|
animationProgress = 0
|
|
|
|
// Notify PanelService immediately
|
|
PanelService.closedPanel(root)
|
|
|
|
// Emit closed signal
|
|
closed()
|
|
|
|
Logger.d("NPanel", "Closing panel with animation", objectName)
|
|
// isPanelOpen will be set to false when animation completes
|
|
}
|
|
|
|
function setPosition() {// Position calculation will be handled here
|
|
// For now, panels will be positioned based on anchors
|
|
}
|
|
|
|
// Loader for panel content
|
|
Loader {
|
|
id: panelContentContainer
|
|
anchors.fill: parent
|
|
// Keep active during close animation
|
|
active: root.isPanelOpen || root.isClosing
|
|
asynchronous: false
|
|
|
|
sourceComponent: Item {
|
|
anchors.fill: parent
|
|
|
|
// Screen-dependent attachment properties (moved from root to avoid race condition)
|
|
// By the time this Loader is active, screen has been assigned by NFullScreenWindow
|
|
readonly property bool couldAttach: Settings.data.ui.panelsAttachedToBar
|
|
readonly property bool couldAttachToBar: {
|
|
if (!Settings.data.ui.panelsAttachedToBar || root.barConfig.backgroundOpacity < 1.0) {
|
|
return false
|
|
}
|
|
|
|
// A panel can only be attached to a bar if there is a bar on that screen
|
|
var monitors = Settings.data.bar.monitors || []
|
|
var result = monitors.length === 0 || monitors.includes(root.screen?.name || "")
|
|
return result
|
|
}
|
|
|
|
// Effective anchor properties (moved from root, depend on couldAttach)
|
|
// These are true when:
|
|
// 1. Explicitly anchored, OR
|
|
// 2. Using button position and bar is on that edge, OR
|
|
// 3. Attached to bar with no explicit anchors (default centering behavior)
|
|
readonly property bool effectivePanelAnchorTop: root.panelAnchorTop || (root.useButtonPosition && root.barPosition === "top") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "top" && !root.barIsVertical)
|
|
readonly property bool effectivePanelAnchorBottom: root.panelAnchorBottom || (root.useButtonPosition && root.barPosition === "bottom") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "bottom" && !root.barIsVertical)
|
|
readonly property bool effectivePanelAnchorLeft: root.panelAnchorLeft || (root.useButtonPosition && root.barPosition === "left") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "left" && root.barIsVertical)
|
|
readonly property bool effectivePanelAnchorRight: root.panelAnchorRight || (root.useButtonPosition && root.barPosition === "right") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "right" && root.barIsVertical)
|
|
|
|
// Expose panelBackground for mask region
|
|
property alias maskRegion: panelBackground
|
|
|
|
// The actual panel background and content
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
NShapedRectangle {
|
|
id: panelBackground
|
|
|
|
backgroundColor: root.panelBackgroundColor
|
|
|
|
Behavior on backgroundColor {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
Behavior on width {
|
|
NumberAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
|
|
// Check if panel has any inverted corners
|
|
readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted
|
|
|
|
// Determine panel attachment type for animation
|
|
readonly property bool isAttachedToFloatingBar: hasInvertedCorners && root.barFloating && couldAttach
|
|
readonly property bool isAttachedToNonFloating: hasInvertedCorners && (!root.barFloating || !couldAttach)
|
|
readonly property bool isDetached: !hasInvertedCorners
|
|
|
|
// Determine closest screen edge to slide from (for full slide animation)
|
|
readonly property string slideDirection: {
|
|
if (!isAttachedToNonFloating)
|
|
return "none"
|
|
|
|
// Priority: If panel is touching the bar (but not touching any screen edge), slide from the bar direction
|
|
// This handles cases where centered panels snap to the bar due to height constraints
|
|
// If touching screen edges, fall through to the distance-based calculation below
|
|
// var touchingAnyScreenEdge = touchingLeftEdge || touchingRightEdge || touchingTopEdge || touchingBottomEdge
|
|
// if (!touchingAnyScreenEdge) {
|
|
if (touchingTopBar && root.barPosition === "top")
|
|
return "top"
|
|
if (touchingBottomBar && root.barPosition === "bottom")
|
|
return "bottom"
|
|
if (touchingLeftBar && root.barPosition === "left")
|
|
return "left"
|
|
if (touchingRightBar && root.barPosition === "right")
|
|
return "right"
|
|
//}
|
|
|
|
// Use panel's center point (barycenter) as reference
|
|
var centerX = x + width / 2
|
|
var centerY = y + height / 2
|
|
|
|
// Calculate actual travel distances (barycenter to screen edge)
|
|
var travelFromTop = centerY
|
|
var travelFromBottom = parent.height - centerY
|
|
var travelFromLeft = centerX
|
|
var travelFromRight = parent.width - centerX
|
|
|
|
// Find minimum travel distance
|
|
var minTravel = Math.min(travelFromTop, travelFromBottom, travelFromLeft, travelFromRight)
|
|
|
|
// Return the direction with least travel distance
|
|
if (minTravel === travelFromTop)
|
|
return "top"
|
|
if (minTravel === travelFromBottom)
|
|
return "bottom"
|
|
if (minTravel === travelFromLeft)
|
|
return "left"
|
|
if (minTravel === travelFromRight)
|
|
return "right"
|
|
return "none"
|
|
}
|
|
|
|
// Animation offset calculation
|
|
readonly property real slideOffset: {
|
|
// Full slide for non-floating attached panels
|
|
if (isAttachedToNonFloating) {
|
|
var distance = (slideDirection === "left" || slideDirection === "right") ? width : height
|
|
return Math.round((1 - root.animationProgress) * distance)
|
|
}
|
|
// Small 40px slide for floating bar attached panels
|
|
if (isAttachedToFloatingBar) {
|
|
return (1 - root.animationProgress) * 40
|
|
}
|
|
// No slide for detached panels
|
|
return 0
|
|
}
|
|
|
|
// Animation properties
|
|
opacity: isAttachedToNonFloating ? Math.min(1, root.animationProgress * 5) : root.animationProgress
|
|
scale: {
|
|
if (isAttachedToNonFloating)
|
|
return 1 // No scale for full slide animation
|
|
if (isAttachedToFloatingBar)
|
|
return 1 // No scale for floating bar (40px slide + opacity only)
|
|
return (0.9 + root.animationProgress * 0.1) // Scale for detached panels
|
|
}
|
|
|
|
// Transform origin for scale animation
|
|
transformOrigin: Item.Center
|
|
|
|
// Slide animation using transform
|
|
transform: Translate {
|
|
x: {
|
|
// Full slide from nearest edge for non-floating attached panels
|
|
if (panelBackground.isAttachedToNonFloating) {
|
|
if (panelBackground.slideDirection === "left")
|
|
return -panelBackground.slideOffset
|
|
if (panelBackground.slideDirection === "right")
|
|
return panelBackground.slideOffset
|
|
return 0
|
|
}
|
|
// Small 40px slide from bar for floating bar attached panels
|
|
if (panelBackground.isAttachedToFloatingBar) {
|
|
if (root.barPosition === "left")
|
|
return -panelBackground.slideOffset
|
|
if (root.barPosition === "right")
|
|
return panelBackground.slideOffset
|
|
}
|
|
return 0
|
|
}
|
|
y: {
|
|
// Full slide from nearest edge for non-floating attached panels
|
|
if (panelBackground.isAttachedToNonFloating) {
|
|
if (panelBackground.slideDirection === "top")
|
|
return -panelBackground.slideOffset
|
|
if (panelBackground.slideDirection === "bottom")
|
|
return panelBackground.slideOffset
|
|
return 0
|
|
}
|
|
// Small 40px slide from bar for floating bar attached panels
|
|
if (panelBackground.isAttachedToFloatingBar) {
|
|
if (root.barPosition === "top")
|
|
return -panelBackground.slideOffset
|
|
if (root.barPosition === "bottom")
|
|
return panelBackground.slideOffset
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
topLeftRadius: Style.radiusL
|
|
topRightRadius: Style.radiusL
|
|
bottomLeftRadius: Style.radiusL
|
|
bottomRightRadius: Style.radiusL
|
|
|
|
// Inverted corners based on bar attachment
|
|
// When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted
|
|
// Also invert corners when touching screen edges (non-floating bar only)
|
|
topLeftInverted: {
|
|
// Bar attachment: only attach to bar if bar opacity >= 1.0 (no color clash)
|
|
var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft))
|
|
// Also detect when panel touches bar edge (e.g., centered panel that's too tall)
|
|
var barTouchInverted = touchingTopBar || touchingLeftBar
|
|
// Screen edge contact: can attach to screen edges even if bar opacity < 1.0
|
|
// For horizontal bars: invert when touching left/right edges
|
|
// For vertical bars: invert when touching top/bottom edges
|
|
var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical))
|
|
// Also invert when touching screen edge opposite to bar (e.g., bottom edge when bar is at top)
|
|
var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
|
|
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
|
|
}
|
|
topRightInverted: {
|
|
var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight))
|
|
var barTouchInverted = touchingTopBar || touchingRightBar
|
|
var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical))
|
|
var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
|
|
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
|
|
}
|
|
bottomLeftInverted: {
|
|
var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft))
|
|
var barTouchInverted = touchingBottomBar || touchingLeftBar
|
|
var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical))
|
|
var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
|
|
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
|
|
}
|
|
bottomRightInverted: {
|
|
var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight))
|
|
var barTouchInverted = touchingBottomBar || touchingRightBar
|
|
var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical))
|
|
var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
|
|
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
|
|
}
|
|
|
|
// Set inverted corner direction based on which edge touches
|
|
// Bar edges: horizontal bars → horizontal curves, vertical bars → vertical curves
|
|
// Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves
|
|
topLeftInvertedDirection: {
|
|
if (touchingLeftEdge && !root.barIsVertical)
|
|
return "vertical"
|
|
if (touchingTopEdge && root.barIsVertical)
|
|
return "horizontal"
|
|
return root.barIsVertical ? "vertical" : "horizontal"
|
|
}
|
|
topRightInvertedDirection: {
|
|
if (touchingRightEdge && !root.barIsVertical)
|
|
return "vertical"
|
|
if (touchingTopEdge && root.barIsVertical)
|
|
return "horizontal"
|
|
return root.barIsVertical ? "vertical" : "horizontal"
|
|
}
|
|
bottomLeftInvertedDirection: {
|
|
if (touchingLeftEdge && !root.barIsVertical)
|
|
return "vertical"
|
|
if (touchingBottomEdge && root.barIsVertical)
|
|
return "horizontal"
|
|
return root.barIsVertical ? "vertical" : "horizontal"
|
|
}
|
|
bottomRightInvertedDirection: {
|
|
if (touchingRightEdge && !root.barIsVertical)
|
|
return "vertical"
|
|
if (touchingBottomEdge && root.barIsVertical)
|
|
return "horizontal"
|
|
return root.barIsVertical ? "vertical" : "horizontal"
|
|
}
|
|
width: {
|
|
var w
|
|
// Priority 1: Content-driven size (dynamic)
|
|
if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) {
|
|
w = contentLoader.item.contentPreferredWidth
|
|
} // Priority 2: Ratio-based size
|
|
else if (root.preferredWidthRatio !== undefined) {
|
|
w = Math.round(Math.max((parent.width || 1920) * root.preferredWidthRatio, root.preferredWidth))
|
|
} // Priority 3: Static preferred width
|
|
else {
|
|
w = root.preferredWidth
|
|
}
|
|
return Math.min(w, (parent.width || 1920) - Style.marginL * 2)
|
|
}
|
|
|
|
height: {
|
|
var h
|
|
// Priority 1: Content-driven size (dynamic)
|
|
if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) {
|
|
h = contentLoader.item.contentPreferredHeight
|
|
} // Priority 2: Ratio-based size
|
|
else if (root.preferredHeightRatio !== undefined) {
|
|
h = Math.round(Math.max((parent.height || 1080) * root.preferredHeightRatio, root.preferredHeight))
|
|
} // Priority 3: Static preferred height
|
|
else {
|
|
h = root.preferredHeight
|
|
}
|
|
return Math.min(h, (parent.height || 1080) - barHeight - Style.marginL * 2)
|
|
}
|
|
|
|
// Detect if panel is touching screen edges
|
|
readonly property bool touchingLeftEdge: couldAttach && x <= 1
|
|
readonly property bool touchingRightEdge: couldAttach && (x + width) >= (parent.width - 1)
|
|
readonly property bool touchingTopEdge: couldAttach && y <= 1
|
|
readonly property bool touchingBottomEdge: couldAttach && (y + height) >= (parent.height - 1)
|
|
|
|
// Detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints)
|
|
readonly property bool touchingTopBar: couldAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(y - (root.barMarginV + barHeight)) <= 1
|
|
readonly property bool touchingBottomBar: couldAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((y + height) - (parent.height - root.barMarginV - barHeight)) <= 1
|
|
readonly property bool touchingLeftBar: couldAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(x - (root.barMarginH + barHeight)) <= 1
|
|
readonly property bool touchingRightBar: couldAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((x + width) - (parent.width - root.barMarginH - barHeight)) <= 1
|
|
|
|
// Position the panel using explicit x/y coordinates (no anchors)
|
|
// This makes coordinates clearer for the click-through mask system
|
|
x: {
|
|
var calculatedX
|
|
|
|
// If useButtonPosition is enabled, align panel X with button
|
|
// Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid
|
|
// after the source panel (e.g., ControlCenter) closes, but we still have valid position data
|
|
if (root.useButtonPosition && parent.width > 0 && width > 0) {
|
|
if (root.barIsVertical) {
|
|
// For vertical bars
|
|
if (couldAttach) {
|
|
// Attached panels: align with bar edge (left or right side)
|
|
if (root.barPosition === "left") {
|
|
// Panel to the right of left bar
|
|
var leftBarEdge = root.barMarginH + barHeight
|
|
// Panel sits right at bar edge (inverted corners align perfectly)
|
|
calculatedX = leftBarEdge
|
|
} else {
|
|
// right
|
|
// Panel to the left of right bar
|
|
var rightBarEdge = parent.width - root.barMarginH - barHeight
|
|
// Panel sits right at bar edge (inverted corners align perfectly)
|
|
calculatedX = rightBarEdge - width
|
|
}
|
|
} else {
|
|
// Detached panels: center on button X position
|
|
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2
|
|
// Clamp to screen bounds with margins, accounting for bar position
|
|
var minX = Style.marginL
|
|
var maxX = parent.width - width - Style.marginL
|
|
|
|
// Account for vertical bar taking up space
|
|
if (root.barPosition === "left") {
|
|
minX = root.barMarginH + barHeight + Style.marginL
|
|
} else if (root.barPosition === "right") {
|
|
maxX = parent.width - root.barMarginH - barHeight - width - Style.marginL
|
|
}
|
|
|
|
panelX = Math.max(minX, Math.min(panelX, maxX))
|
|
calculatedX = panelX
|
|
}
|
|
} else {
|
|
// For horizontal bars, center panel on button X position
|
|
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2
|
|
// Clamp to bar bounds (account for floating bar margins)
|
|
// When attached, panel should not extend beyond bar edges
|
|
if (couldAttach) {
|
|
// Inverted corners with horizontal direction extend left/right by radiusL
|
|
// When bar is floating, it also has rounded corners, so we need extra insets
|
|
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
|
|
var barLeftEdge = root.barMarginH + cornerInset
|
|
var barRightEdge = parent.width - root.barMarginH - cornerInset
|
|
panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width))
|
|
} else {
|
|
panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL))
|
|
}
|
|
calculatedX = panelX
|
|
}
|
|
} else {
|
|
|
|
// Standard anchor positioning
|
|
Logger.d("NPanel", "Fallback to standard anchor positioning")
|
|
|
|
if (root.panelAnchorHorizontalCenter) {
|
|
Logger.d("NPanel", " -> Horizontal center")
|
|
// Center horizontally, accounting for bar position and margins
|
|
if (root.barIsVertical) {
|
|
// For vertical bars, center in the available space not occupied by the bar
|
|
if (root.barPosition === "left") {
|
|
var availableStart = root.barMarginH + barHeight
|
|
var availableWidth = parent.width - availableStart
|
|
calculatedX = availableStart + (availableWidth - width) / 2
|
|
} else if (root.barPosition === "right") {
|
|
var availableWidth = parent.width - root.barMarginH - barHeight
|
|
calculatedX = (availableWidth - width) / 2
|
|
} else {
|
|
// No vertical bar, center normally
|
|
calculatedX = (parent.width - width) / 2
|
|
}
|
|
} else {
|
|
// For horizontal bars or no bar, center normally
|
|
calculatedX = (parent.width - width) / 2
|
|
}
|
|
} else if (effectivePanelAnchorRight) {
|
|
Logger.d("NPanel", " -> Right anchor")
|
|
// When attached to right vertical bar, position next to bar (like useButtonPosition does)
|
|
if (couldAttach && root.barIsVertical && root.barPosition === "right") {
|
|
var rightBarEdge = parent.width - root.barMarginH - barHeight
|
|
calculatedX = rightBarEdge - width
|
|
} else if (couldAttach) {
|
|
// Attach to right screen edge
|
|
calculatedX = parent.width - width
|
|
} else {
|
|
// Detached: use margin
|
|
calculatedX = parent.width - width - Style.marginL
|
|
}
|
|
} else if (effectivePanelAnchorLeft) {
|
|
Logger.d("NPanel", " -> Left anchor")
|
|
// When attached to left vertical bar, position next to bar (like useButtonPosition does)
|
|
if (couldAttach && root.barIsVertical && root.barPosition === "left") {
|
|
var leftBarEdge = root.barMarginH + barHeight
|
|
calculatedX = leftBarEdge
|
|
} else if (couldAttach) {
|
|
// Attach to left screen edge
|
|
calculatedX = 0
|
|
} else {
|
|
// Detached: use margin
|
|
calculatedX = Style.marginL
|
|
}
|
|
} else {
|
|
// No explicit anchor: default to centering on bar
|
|
Logger.d("NPanel", " -> Default to center (no explicit anchor)")
|
|
|
|
// For horizontal bars: center horizontally
|
|
// For vertical bars: center horizontally in available space
|
|
if (root.barIsVertical) {
|
|
// Center in the space not occupied by the bar
|
|
if (root.barPosition === "left") {
|
|
var availableStart = root.barMarginH + barHeight
|
|
var availableWidth = parent.width - availableStart - Style.marginL
|
|
calculatedX = availableStart + (availableWidth - width) / 2
|
|
} else {
|
|
// right
|
|
var availableWidth = parent.width - root.barMarginH - barHeight - Style.marginL
|
|
calculatedX = Style.marginL + (availableWidth - width) / 2
|
|
}
|
|
} else {
|
|
// For horizontal bars: center horizontally, respect bar margins if attached
|
|
if (couldAttach) {
|
|
// When attached, respect bar bounds (like button position does)
|
|
var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0)
|
|
var barLeftEdge = root.barMarginH + cornerInset
|
|
var barRightEdge = parent.width - root.barMarginH - cornerInset
|
|
var centeredX = (parent.width - width) / 2
|
|
calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width))
|
|
} else {
|
|
calculatedX = (parent.width - width) / 2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edge snapping: snap to screen edges if close (only when attached and bar is not floating)
|
|
if (couldAttach && !root.barFloating && parent.width > 0 && width > 0) {
|
|
// Calculate edge positions accounting for bar position
|
|
// For vertical bars (left/right), we need to position panels AFTER the bar, not behind it
|
|
var leftEdgePos = root.barMarginH
|
|
if (root.barPosition === "left") {
|
|
// Bar is on the left, so left edge is after the bar
|
|
leftEdgePos = root.barMarginH + barHeight
|
|
}
|
|
|
|
var rightEdgePos = parent.width - root.barMarginH - width
|
|
if (root.barPosition === "right") {
|
|
// Bar is on the right, so right edge is before the bar
|
|
rightEdgePos = parent.width - root.barMarginH - barHeight - width
|
|
}
|
|
|
|
// Snap to left edge if within snap distance
|
|
if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) {
|
|
calculatedX = leftEdgePos
|
|
} // Snap to right edge if within snap distance
|
|
else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) {
|
|
calculatedX = rightEdgePos
|
|
}
|
|
}
|
|
|
|
return calculatedX
|
|
}
|
|
|
|
y: {
|
|
var calculatedY
|
|
|
|
// If useButtonPosition is enabled, position panel relative to bar
|
|
// Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid
|
|
// after the source panel (e.g., ControlCenter) closes, but we still have valid position data
|
|
if (root.useButtonPosition && parent.height > 0 && height > 0) {
|
|
if (root.barPosition === "top") {
|
|
// Panel below top bar
|
|
var topBarEdge = root.barMarginV + barHeight
|
|
if (couldAttach) {
|
|
// Panel sits right at bar edge (inverted corners align perfectly)
|
|
calculatedY = topBarEdge
|
|
} else {
|
|
calculatedY = topBarEdge + Style.marginM
|
|
}
|
|
} else if (root.barPosition === "bottom") {
|
|
// Panel above bottom bar
|
|
var bottomBarEdge = parent.height - root.barMarginV - barHeight
|
|
if (couldAttach) {
|
|
// Panel sits right at bar edge (inverted corners align perfectly)
|
|
calculatedY = bottomBarEdge - height
|
|
} else {
|
|
calculatedY = bottomBarEdge - height - Style.marginM
|
|
}
|
|
} else if (root.barIsVertical) {
|
|
// For vertical bars, center panel on button Y position
|
|
var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2
|
|
// Clamp to bar bounds (account for floating bar margins and inverted corners)
|
|
var extraPadding = (couldAttach && root.barFloating) ? Style.radiusL : 0
|
|
if (couldAttach) {
|
|
// When attached, panel should not extend beyond bar edges (accounting for floating margins)
|
|
// Inverted corners with vertical direction extend up/down by radiusL
|
|
// When bar is floating, it also has rounded corners, so we need extra inset
|
|
var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0)
|
|
var barTopEdge = root.barMarginV + cornerInset
|
|
var barBottomEdge = parent.height - root.barMarginV - cornerInset
|
|
panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - height))
|
|
} else {
|
|
panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding))
|
|
}
|
|
calculatedY = panelY
|
|
}
|
|
} else {
|
|
|
|
// Standard anchor positioning
|
|
// Calculate bar offset for detached panels - they should never overlap the bar
|
|
var barOffset = 0
|
|
if (!couldAttach) {
|
|
// For detached panels, always account for bar position
|
|
if (root.barPosition === "top") {
|
|
barOffset = root.barMarginV + barHeight + Style.marginM
|
|
} else if (root.barPosition === "bottom") {
|
|
barOffset = root.barMarginV + barHeight + Style.marginM
|
|
}
|
|
} else {
|
|
// For attached panels with explicit anchors
|
|
if (effectivePanelAnchorTop && root.barPosition === "top") {
|
|
// When attached to top bar: position right at bar edge (inverted corners align perfectly)
|
|
calculatedY = root.barMarginV + barHeight
|
|
} else if (effectivePanelAnchorBottom && root.barPosition === "bottom") {
|
|
// When attached to bottom bar: position right at bar edge (inverted corners align perfectly)
|
|
calculatedY = parent.height - root.barMarginV - barHeight - height
|
|
} else if (!root.hasExplicitVerticalAnchor) {
|
|
// No explicit vertical anchor AND attached: default to attaching to bar edge
|
|
if (root.barPosition === "top") {
|
|
// Attach to top bar
|
|
calculatedY = root.barMarginV + barHeight
|
|
} else if (root.barPosition === "bottom") {
|
|
// Attach to bottom bar
|
|
calculatedY = parent.height - root.barMarginV - barHeight - height
|
|
}
|
|
// For vertical bars with no explicit anchor: fall through to center vertically on bar
|
|
}
|
|
}
|
|
|
|
// Continue if calculatedY was already set above, or proceed with anchor positioning
|
|
if (calculatedY === undefined) {
|
|
if (root.panelAnchorVerticalCenter) {
|
|
// Center vertically, accounting for bar position and margins
|
|
if (!root.barIsVertical) {
|
|
// For horizontal bars, center in the available space not occupied by the bar
|
|
if (root.barPosition === "top") {
|
|
var availableStart = root.barMarginV + barHeight
|
|
var availableHeight = parent.height - availableStart
|
|
calculatedY = availableStart + (availableHeight - height) / 2
|
|
} else if (root.barPosition === "bottom") {
|
|
var availableHeight = parent.height - root.barMarginV - barHeight
|
|
calculatedY = (availableHeight - height) / 2
|
|
} else {
|
|
// No horizontal bar, center normally
|
|
calculatedY = (parent.height - height) / 2
|
|
}
|
|
} else {
|
|
// For vertical bars or no bar, center normally
|
|
calculatedY = (parent.height - height) / 2
|
|
}
|
|
} else if (effectivePanelAnchorTop) {
|
|
// When couldAttach=true, attach to top screen edge; otherwise use margin
|
|
if (couldAttach) {
|
|
calculatedY = 0
|
|
} else {
|
|
// Only apply barOffset if bar is also at top (to avoid overlapping)
|
|
var topBarOffset = (root.barPosition === "top") ? barOffset : 0
|
|
calculatedY = topBarOffset + Style.marginL
|
|
}
|
|
} else if (effectivePanelAnchorBottom) {
|
|
// When couldAttach=true, attach to bottom screen edge; otherwise use margin
|
|
if (couldAttach) {
|
|
calculatedY = parent.height - height
|
|
} else {
|
|
// Only apply barOffset if bar is also at bottom (to avoid overlapping)
|
|
var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0
|
|
calculatedY = parent.height - height - bottomBarOffset - Style.marginL
|
|
}
|
|
} else {
|
|
// No explicit vertical anchor
|
|
if (root.barIsVertical) {
|
|
// For vertical bars: center vertically on bar
|
|
if (couldAttach) {
|
|
// When attached, respect bar bounds
|
|
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
|
|
var barTopEdge = root.barMarginV + cornerInset
|
|
var barBottomEdge = parent.height - root.barMarginV - cornerInset
|
|
var centeredY = (parent.height - height) / 2
|
|
calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height))
|
|
} else {
|
|
calculatedY = (parent.height - height) / 2
|
|
}
|
|
} else {
|
|
// For horizontal bars: attach to bar edge by default
|
|
if (couldAttach && !root.barIsVertical) {
|
|
if (root.barPosition === "top") {
|
|
calculatedY = root.barMarginV + barHeight
|
|
} else if (root.barPosition === "bottom") {
|
|
calculatedY = parent.height - root.barMarginV - barHeight - height
|
|
}
|
|
} else {
|
|
// Detached or no bar position: use default positioning
|
|
if (root.barPosition === "top") {
|
|
calculatedY = barOffset + Style.marginL
|
|
} else if (root.barPosition === "bottom") {
|
|
calculatedY = Style.marginL
|
|
} else {
|
|
calculatedY = Style.marginL
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edge snapping: snap to screen edges if close (only when attached and bar is not floating)
|
|
if (couldAttach && !root.barFloating && parent.height > 0 && height > 0) {
|
|
// Calculate edge positions accounting for bar position
|
|
// For horizontal bars (top/bottom), we need to position panels AFTER the bar, not behind it
|
|
var topEdgePos = root.barMarginV
|
|
if (root.barPosition === "top") {
|
|
// Bar is on the top, so top edge is after the bar
|
|
topEdgePos = root.barMarginV + barHeight
|
|
}
|
|
|
|
var bottomEdgePos = parent.height - root.barMarginV - height
|
|
if (root.barPosition === "bottom") {
|
|
// Bar is on the bottom, so bottom edge is before the bar
|
|
bottomEdgePos = parent.height - root.barMarginV - barHeight - height
|
|
}
|
|
|
|
// Snap to top edge if within snap distance
|
|
if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) {
|
|
calculatedY = topEdgePos
|
|
} // Snap to bottom edge if within snap distance
|
|
else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) {
|
|
calculatedY = bottomEdgePos
|
|
}
|
|
}
|
|
|
|
return calculatedY
|
|
}
|
|
|
|
// MouseArea to catch clicks on the panel and prevent them from reaching the background
|
|
// This prevents closing the panel when clicking inside it
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
z: -1 // Behind content, but on the panel background
|
|
onClicked: {
|
|
|
|
// Accept and ignore - prevents propagation to background
|
|
}
|
|
}
|
|
|
|
// Panel content loader
|
|
Loader {
|
|
id: contentLoader
|
|
anchors.fill: parent
|
|
sourceComponent: root.panelContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|