Refactor: Panels and Bar background are now drawn separately with Shapes.

This commit is contained in:
ItsLemmy
2025-11-06 10:41:48 -05:00
parent 76a182a90c
commit e29c6ee1a6
109 changed files with 2381 additions and 2192 deletions
+44
View File
@@ -0,0 +1,44 @@
import QtQuick
import qs.Commons
Item {
id: root
property color fillColor: Color.mPrimary
property color strokeColor: Color.mOnSurface
property int strokeWidth: 0
property var values: []
property bool vertical: false
// Minimum signal properties
property bool showMinimumSignal: false
property real minimumSignalValue: 0.05 // Default to 5% of height
// Pre compute horizontal mirroring
readonly property int valuesCount: values.length
readonly property int totalBars: valuesCount * 2
readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0
Repeater {
model: root.totalBars
Rectangle {
// The first half of bars are a mirror image (reversed values array).
// The second half of bars are in normal order.
property int valueIndex: index < root.valuesCount ? root.valuesCount - 1 - index // Mirrored half
: index - root.valuesCount // Normal half
property real rawAmp: root.values[valueIndex]
property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp
color: root.fillColor
border.color: root.strokeColor
border.width: root.strokeWidth
antialiasing: true
width: vertical ? root.width * amp : root.barSlotSize * 0.5
height: vertical ? root.barSlotSize * 0.5 : root.height * amp
x: vertical ? root.width - width : index * root.barSlotSize + (root.barSlotSize * 0.25)
y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.height - height
}
}
}
@@ -0,0 +1,49 @@
import QtQuick
import qs.Commons
Item {
id: root
property color fillColor: Color.mPrimary
property color strokeColor: Color.mOnSurface
property int strokeWidth: 0
property var values: []
property bool vertical: false
// Minimum signal properties
property bool showMinimumSignal: false
property real minimumSignalValue: 0.05 // Default to 5% of height
// Pre-compute mirroring
readonly property int valuesCount: values.length
readonly property int totalBars: valuesCount * 2
readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0
readonly property real centerY: height / 2
readonly property real centerX: width / 2
Repeater {
model: root.totalBars
Rectangle {
// The first half of bars are a mirror image (reversed values array).
// The second half of bars are in normal order.
property int valueIndex: index < root.valuesCount ? root.valuesCount - 1 - index // Mirrored half
: index - root.valuesCount // Normal half
property real rawAmp: root.values[valueIndex]
property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp
property real barSize: (vertical ? root.width : root.height) * amp
color: root.fillColor
border.color: root.strokeColor
border.width: root.strokeWidth
antialiasing: true
width: vertical ? barSize : root.barSlotSize * 0.8
height: vertical ? root.barSlotSize * 0.8 : barSize
x: vertical ? root.centerX - (barSize / 2) : index * root.barSlotSize + (root.barSlotSize * 0.25)
y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.centerY - (barSize / 2)
}
}
}
+125
View File
@@ -0,0 +1,125 @@
import QtQuick
import qs.Commons
Item {
id: root
property color fillColor: Color.mPrimary
property color strokeColor: Color.mOnSurface
property int strokeWidth: 0
property var values: []
property bool vertical: false
// Minimum signal properties
property bool showMinimumSignal: false
property real minimumSignalValue: 0.05 // Default to 5% of height
// Redraw when necessary
onWidthChanged: canvas.requestPaint()
onHeightChanged: canvas.requestPaint()
onValuesChanged: canvas.requestPaint()
onFillColorChanged: canvas.requestPaint()
onStrokeColorChanged: canvas.requestPaint()
onShowMinimumSignalChanged: canvas.requestPaint()
onMinimumSignalValueChanged: canvas.requestPaint()
onVerticalChanged: canvas.requestPaint()
Canvas {
id: canvas
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
if (values.length === 0) {
return
}
// Apply minimum signal if enabled
var processedValues = values.map(function (v) {
return (root.showMinimumSignal && v === 0) ? root.minimumSignalValue : v
})
// Create the mirrored values
const partToMirror = processedValues.slice(1).reverse()
const mirroredValues = partToMirror.concat(processedValues)
if (mirroredValues.length < 2) {
return
}
ctx.fillStyle = root.fillColor
ctx.strokeStyle = root.strokeColor
ctx.lineWidth = root.strokeWidth
const count = mirroredValues.length
if (root.vertical) {
// Vertical orientation
const stepY = height / (count - 1)
const centerX = width / 2
const amplitude = width / 2
ctx.beginPath()
// Draw the left half of the waveform from top to bottom
var xOffset = mirroredValues[0] * amplitude
ctx.moveTo(centerX - xOffset, 0)
for (var i = 1; i < count; i++) {
const y = i * stepY
xOffset = mirroredValues[i] * amplitude
const x = centerX - xOffset
ctx.lineTo(x, y)
}
// Draw the right half of the waveform from bottom to top to create a closed shape
for (var i = count - 1; i >= 0; i--) {
const y = i * stepY
xOffset = mirroredValues[i] * amplitude
const x = centerX + xOffset // Mirrored across the center
ctx.lineTo(x, y)
}
ctx.closePath()
} else {
// Horizontal orientation
const stepX = width / (count - 1)
const centerY = height / 2
const amplitude = height / 2
ctx.beginPath()
// Draw the top half of the waveform from left to right
var yOffset = mirroredValues[0] * amplitude
ctx.moveTo(0, centerY - yOffset)
for (var i = 1; i < count; i++) {
const x = i * stepX
yOffset = mirroredValues[i] * amplitude
const y = centerY - yOffset
ctx.lineTo(x, y)
}
// Draw the bottom half of the waveform from right to left to create a closed shape
for (var i = count - 1; i >= 0; i--) {
const x = i * stepX
yOffset = mirroredValues[i] * amplitude
const y = centerY + yOffset // Mirrored across the center
ctx.lineTo(x, y)
}
ctx.closePath()
}
// --- Render the path ---
if (root.fillColor.a > 0) {
ctx.fill()
}
if (root.strokeWidth > 0) {
ctx.stroke()
}
}
}
}
-616
View File
@@ -1,616 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
/**
* NFullScreenWindow - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
required property var barComponent
required property var panelComponents
Component.onCompleted: {
Logger.d("NFullScreenWindow", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y)
}
// Debug: Log mask region changes
onMaskChanged: {
Logger.d("NFullScreenWindow", "Mask changed!")
Logger.d("NFullScreenWindow", " Bar region:", barLoader.item?.barRegion)
Logger.d("NFullScreenWindow", " Panel count:", panelsRepeater.count)
for (var i = 0; i < panelsRepeater.count; i++) {
var panelItem = panelsRepeater.itemAt(i)?.item
Logger.d("NFullScreenWindow", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion)
}
}
// Wayland
// Always use Exclusive keyboard focus when a panel is open
// This ensures all keyboard shortcuts work reliably (Escape, etc.)
// The centralized shortcuts in this window handle delegation to panels
WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.layer: Settings.data.ui.panelsOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that
anchors {
top: true
bottom: true
left: true
right: true
}
// Desktop dimming when panels are open
property bool dimDesktop: Settings.data.general.dimDesktop
property bool isPanelOpen: PanelService.openedPanel !== null
color: {
if (dimDesktop && isPanelOpen) {
return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy)
}
return Color.transparent
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
function updateMask() {
// Build the regions list
var regionsList = [barMaskRegion]
// Add background region if a panel is open
// This makes the background clickable (not click-through) so we can detect clicks to close panels
if (root.isPanelOpen) {
regionsList.push(backgroundMaskRegion)
}
// Add regions for each open panel
// Only include panels that are open AND not closing (to allow click-through during close animation)
for (var i = 0; i < panelMaskRepeater.count; i++) {
var wrapperItem = panelMaskRepeater.itemAt(i)
if (wrapperItem && wrapperItem.maskRegion) {
var panelItem = wrapperItem.panelItem
if (panelItem && panelItem.isPanelOpen && !panelItem.isClosing) {
var panelRegion = panelItem.panelRegion
// Update the mask region's coordinates from the panel's actual region
if (panelRegion) {
wrapperItem.maskRegion.x = panelRegion.x
wrapperItem.maskRegion.y = panelRegion.y
wrapperItem.maskRegion.width = panelRegion.width
wrapperItem.maskRegion.height = panelRegion.height
regionsList.push(wrapperItem.maskRegion)
}
}
}
}
// Update the mask's regions
clickableMask.regions = regionsList
}
// Listen to PanelService to update mask when panels open/close
Connections {
target: PanelService
function onWillOpen() {
root.updateMask()
}
function onDidClose() {
// Delay mask update to ensure panel's isPanelOpen is updated first
Qt.callLater(() => root.updateMask())
}
}
// Background region - for closing panels when clicking outside (separate from mask)
Region {
id: backgroundMaskRegion
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Subtract
}
// Smart mask: Make everything click-through except bar and open panels
mask: Region {
id: clickableMask
// Cover entire window (everything is masked/click-through)
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Xor
// Regions list is set programmatically in updateMask()
// Initially just the bar
regions: [barMaskRegion]
// Bar region - subtract bar area from mask
Region {
id: barMaskRegion
property var barRegion: barLoader.item && barLoader.item.barRegion ? barLoader.item.barRegion : null
x: barRegion ? barRegion.x : 0
y: barRegion ? barRegion.y : 0
width: barRegion ? barRegion.width : 0
height: barRegion ? barRegion.height : 0
intersection: Intersection.Subtract
// Update mask when bar geometry changes
onWidthChanged: Qt.callLater(() => root.updateMask())
onHeightChanged: Qt.callLater(() => root.updateMask())
}
}
// Container for panel mask regions (created dynamically)
Item {
id: panelMaskRegions
// Create a Region for each panel
Repeater {
id: panelMaskRepeater
model: panelsRepeater.count
delegate: Item {
required property int index
property var panelItem: panelsRepeater.itemAt(index)?.item
property var region: panelItem && panelItem.panelRegion ? panelItem.panelRegion : null
// The actual mask region as a child
property alias maskRegion: panelMask
Region {
id: panelMask
// Coordinates are set programmatically in updateMask()
intersection: Intersection.Subtract
}
}
}
}
// Container for all UI elements
Item {
id: container
width: root.width
height: root.height
// Apply shadow effect
layer.enabled: Settings.data.general.enableShadows
layer.smooth: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowOpacity: Style.shadowOpacity
shadowHorizontalOffset: Style.shadowHorizontalOffset
shadowVerticalOffset: Style.shadowVerticalOffset
shadowColor: Color.black
blur: Style.shadowBlur
blurMax: Style.shadowBlurMax
}
// Screen corners (integrated to avoid separate PanelWindow)
// Always positioned at actual screen edges
Loader {
id: screenCornersLoader
active: Settings.data.general.showScreenCorners
anchors.fill: parent
z: 1000 // Very high z-index to be on top of everything
sourceComponent: Item {
id: cornersRoot
anchors.fill: parent
property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Color.black : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
property real cornerRadius: Style.screenRadius
property real cornerSize: Style.screenRadius
// Top-left concave corner
Canvas {
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = Color.white
ctx.beginPath()
ctx.arc(width, height, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Top-right concave corner
Canvas {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = Color.white
ctx.beginPath()
ctx.arc(0, height, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Bottom-left concave corner
Canvas {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = Color.white
ctx.beginPath()
ctx.arc(width, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Bottom-right concave corner
Canvas {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = Color.white
ctx.beginPath()
ctx.arc(0, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Repaint all corners when color or radius changes
onCornerColorChanged: {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
if (topRightCorner.available)
topRightCorner.requestPaint()
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
onCornerRadiusChanged: {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
if (topRightCorner.available)
topRightCorner.requestPaint()
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
}
}
// Background MouseArea for closing panels when clicking outside
// Active whenever a panel is open - the mask ensures it only receives clicks when panel is open
MouseArea {
anchors.fill: parent
enabled: root.isPanelOpen
onClicked: {
if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
z: 0 // Behind panels and bar
}
// All panels (as Items, not PanelWindows)
Repeater {
id: panelsRepeater
model: root.panelComponents
delegate: Loader {
id: panelLoader
// Lazy load panels - only create when first requested
// Panel stays loaded once created for faster subsequent opens
active: false
asynchronous: false
sourceComponent: modelData.component
// Fill the container so panels have proper parent dimensions
anchors.fill: parent
// Panel properties binding
property var panelScreen: root.screen
property string panelId: modelData.id
property int panelZIndex: modelData.zIndex || 50
property bool hasBeenRequested: false
Component.onCompleted: {
// Register the loader immediately so PanelService can load it on-demand
var objectName = panelId + "-" + (panelScreen?.name || "unknown")
PanelService.registerPanelLoader(panelLoader, objectName)
}
// Activate loader when panel is first requested
function ensureLoaded() {
if (!hasBeenRequested) {
Logger.d("NFullScreenWindow", "Loading panel on-demand:", panelId)
hasBeenRequested = true
active = true
}
}
onLoaded: {
if (item) {
// Set unique objectName per screen BEFORE registration: "calendarPanel-DP-1"
item.objectName = panelId + "-" + (panelScreen?.name || "unknown")
item.screen = panelScreen
PanelService.registerPanel(item)
Logger.d("NFullScreenWindow", "Panel loaded with objectName:", item.objectName, "on screen:", panelScreen?.name)
}
}
}
}
// Bar (always on top)
Loader {
id: barLoader
asynchronous: false
sourceComponent: root.barComponent
// Keep bar loaded but hide it when BarService.isVisible is false
// This allows panels to remain accessible via IPC
visible: BarService.isVisible
// Fill parent to provide dimensions for Bar to reference
anchors.fill: parent
property ShellScreen screen: root.screen
onLoaded: {
Logger.d("NFullScreenWindow", "Bar loaded:", item !== null)
if (item) {
Logger.d("NFullScreenWindow", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height)
// Bind screen to bar component (use binding for reactivity)
item.screen = Qt.binding(function () {
return barLoader.screen
})
}
}
}
}
// Centralized keyboard shortcuts - delegate to opened panel
// This ensures shortcuts work regardless of panel focus state
Shortcut {
sequence: "Escape"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) {
PanelService.openedPanel.onEscapePressed()
} else if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
}
Shortcut {
sequence: "Tab"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) {
PanelService.openedPanel.onTabPressed()
}
}
}
Shortcut {
sequence: "Shift+Tab"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) {
PanelService.openedPanel.onShiftTabPressed()
}
}
}
Shortcut {
sequence: "Up"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) {
PanelService.openedPanel.onUpPressed()
}
}
}
Shortcut {
sequence: "Down"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) {
PanelService.openedPanel.onDownPressed()
}
}
}
Shortcut {
sequence: "Return"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) {
PanelService.openedPanel.onReturnPressed()
}
}
}
Shortcut {
sequence: "Enter"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) {
PanelService.openedPanel.onReturnPressed()
}
}
}
Shortcut {
sequence: "Home"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) {
PanelService.openedPanel.onHomePressed()
}
}
}
Shortcut {
sequence: "End"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) {
PanelService.openedPanel.onEndPressed()
}
}
}
Shortcut {
sequence: "PgUp"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) {
PanelService.openedPanel.onPageUpPressed()
}
}
}
Shortcut {
sequence: "PgDown"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) {
PanelService.openedPanel.onPageDownPressed()
}
}
}
Shortcut {
sequence: "Ctrl+J"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) {
PanelService.openedPanel.onCtrlJPressed()
}
}
}
Shortcut {
sequence: "Ctrl+K"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) {
PanelService.openedPanel.onCtrlKPressed()
}
}
}
Shortcut {
sequence: "Left"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) {
PanelService.openedPanel.onLeftPressed()
}
}
}
Shortcut {
sequence: "Right"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) {
PanelService.openedPanel.onRightPressed()
}
}
}
}
-858
View File
@@ -1,858 +0,0 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services
/**
* NPanel for use within NFullScreenWindow
*/
Item {
id: root
// Screen property provided by NFullScreenWindow
property ShellScreen screen: null
// Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap
property real edgeSnapDistance: 50
property Component panelContent: null
// Panel size properties
property real preferredWidth: 700
property real preferredHeight: 900
property real preferredWidthRatio
property real preferredHeightRatio
property color panelBackgroundColor: Color.mSurface
property color panelBorderColor: Color.mOutline
property var buttonItem: null
// Anchoring properties
property bool panelAnchorHorizontalCenter: false
property bool panelAnchorVerticalCenter: false
property bool panelAnchorTop: false
property bool panelAnchorBottom: false
property bool panelAnchorLeft: false
property bool panelAnchorRight: false
// Button position properties
property bool useButtonPosition: false
property point buttonPosition: Qt.point(0, 0)
property int buttonWidth: 0
property int buttonHeight: 0
// Track whether panel is open
property bool isPanelOpen: false
// Animation properties
property real animationProgress: 0
property bool isClosing: false
// Per-panel animation overrides
property bool disableScaleAnimation: false
property bool disableSlideAnimation: false
// If >= 0, use this pixel distance for slide instead of default
property int customSlideDistance: -1
// Keyboard event handlers - override these in specific panels to handle shortcuts
// These are called from NFullScreenWindow's centralized shortcuts
function onEscapePressed() {
close()
}
function onTabPressed() {}
function onShiftTabPressed() {}
function onUpPressed() {}
function onDownPressed() {}
function onLeftPressed() {}
function onRightPressed() {}
function onReturnPressed() {}
function onHomePressed() {}
function onEndPressed() {}
function onPageUpPressed() {}
function onPageDownPressed() {}
function onCtrlJPressed() {}
function onCtrlKPressed() {}
Behavior on animationProgress {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuint
onRunningChanged: {
// When close animation finishes, actually hide the panel
if (!running && root.isClosing) {
root.isClosing = false
root.isPanelOpen = false
}
}
}
}
// Expose panel region for click-through mask (only when open)
readonly property var panelRegion: panelContentContainer.item?.maskRegion || null
readonly property string barPosition: Settings.data.bar.position
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool barFloating: Settings.data.bar.floating || false
readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
// Helper to detect if any anchor is explicitly set
readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight
readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom
signal opened
signal closed
// Panel visibility and sizing
// Keep visible during close animation
visible: isPanelOpen || isClosing
width: parent ? parent.width : 0
height: parent ? parent.height : 0
// Panel control functions
function toggle(buttonItem, buttonName) {
if (!isPanelOpen) {
open(buttonItem, buttonName)
} else {
close()
}
}
function open(buttonItem, buttonName) {
if (!buttonItem && buttonName) {
buttonItem = BarService.lookupWidget(buttonName, screen.name)
}
if (buttonItem) {
root.buttonItem = buttonItem
// Map button position to screen coordinates
var buttonPos = buttonItem.mapToItem(null, 0, 0)
root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y)
root.buttonWidth = buttonItem.width
root.buttonHeight = buttonItem.height
root.useButtonPosition = true
} else {
// No button provided: reset button position mode
root.buttonItem = null
root.useButtonPosition = false
}
setPosition()
isPanelOpen = true
animationProgress = 1
// Notify PanelService
PanelService.willOpenPanel(root)
// Delay the opened signal to ensure content is fully loaded
// This ensures Component.onCompleted of the loaded content runs first
Qt.callLater(() => {
opened()
})
Logger.d("NPanel", "Opened panel", objectName)
Logger.d("NPanel", " Root size:", width, "x", height)
}
function close() {
// Start close animation
isClosing = true
animationProgress = 0
// Notify PanelService immediately
PanelService.closedPanel(root)
// Emit closed signal
closed()
Logger.d("NPanel", "Closing panel with animation", objectName)
// isPanelOpen will be set to false when animation completes
}
function setPosition() {// Position calculation will be handled here
// For now, panels will be positioned based on anchors
}
// Loader for panel content
Loader {
id: panelContentContainer
anchors.fill: parent
// Keep active during close animation
active: root.isPanelOpen || root.isClosing
asynchronous: false
sourceComponent: Item {
anchors.fill: parent
// Screen-dependent attachment properties (moved from root to avoid race condition)
// By the time this Loader is active, screen has been assigned by NFullScreenWindow
readonly property bool couldAttach: Settings.data.ui.panelsAttachedToBar
readonly property bool couldAttachToBar: {
if (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity < 1.0) {
return false
}
// A panel can only be attached to a bar if there is a bar on that screen
var monitors = Settings.data.bar.monitors || []
var result = monitors.length === 0 || monitors.includes(root.screen?.name || "")
return result
}
// Effective anchor properties (moved from root, depend on couldAttach)
// These are true when:
// 1. Explicitly anchored, OR
// 2. Using button position and bar is on that edge, OR
// 3. Attached to bar with no explicit anchors (default centering behavior)
readonly property bool effectivePanelAnchorTop: root.panelAnchorTop || (root.useButtonPosition && root.barPosition === "top") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "top" && !root.barIsVertical)
readonly property bool effectivePanelAnchorBottom: root.panelAnchorBottom || (root.useButtonPosition && root.barPosition === "bottom") || (couldAttach && !root.hasExplicitVerticalAnchor && root.barPosition === "bottom" && !root.barIsVertical)
readonly property bool effectivePanelAnchorLeft: root.panelAnchorLeft || (root.useButtonPosition && root.barPosition === "left") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "left" && root.barIsVertical)
readonly property bool effectivePanelAnchorRight: root.panelAnchorRight || (root.useButtonPosition && root.barPosition === "right") || (couldAttach && !root.hasExplicitHorizontalAnchor && root.barPosition === "right" && root.barIsVertical)
// Expose panelBackground for mask region
property alias maskRegion: panelBackground
// The actual panel background and content
Item {
anchors.fill: parent
NShapedRectangle {
id: panelBackground
backgroundColor: root.panelBackgroundColor
Behavior on backgroundColor {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
Behavior on width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
// Check if panel has any inverted corners
readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted
// Determine panel attachment type for animation
readonly property bool isAttachedToFloatingBar: hasInvertedCorners && root.barFloating && couldAttach
readonly property bool isAttachedToNonFloating: hasInvertedCorners && (!root.barFloating || !couldAttach)
readonly property bool isDetached: !hasInvertedCorners
// Determine closest screen edge to slide from (for full slide animation)
readonly property string slideDirection: {
if (!isAttachedToNonFloating)
return "none"
// Priority: If panel is touching the bar (but not touching any screen edge), slide from the bar direction
// This handles cases where centered panels snap to the bar due to height constraints
// If touching screen edges, fall through to the distance-based calculation below
// var touchingAnyScreenEdge = touchingLeftEdge || touchingRightEdge || touchingTopEdge || touchingBottomEdge
// if (!touchingAnyScreenEdge) {
if (touchingTopBar && root.barPosition === "top")
return "top"
if (touchingBottomBar && root.barPosition === "bottom")
return "bottom"
if (touchingLeftBar && root.barPosition === "left")
return "left"
if (touchingRightBar && root.barPosition === "right")
return "right"
//}
// Use panel's center point (barycenter) as reference
var centerX = x + width / 2
var centerY = y + height / 2
// Calculate actual travel distances (barycenter to screen edge)
var travelFromTop = centerY
var travelFromBottom = parent.height - centerY
var travelFromLeft = centerX
var travelFromRight = parent.width - centerX
// Find minimum travel distance
var minTravel = Math.min(travelFromTop, travelFromBottom, travelFromLeft, travelFromRight)
// Return the direction with least travel distance
if (minTravel === travelFromTop)
return "top"
if (minTravel === travelFromBottom)
return "bottom"
if (minTravel === travelFromLeft)
return "left"
if (minTravel === travelFromRight)
return "right"
return "none"
}
// Animation offset calculation
readonly property real slideOffset: {
if (root.disableSlideAnimation)
return 0
if (root.customSlideDistance >= 0) {
return (1 - root.animationProgress) * root.customSlideDistance
}
// Full slide for non-floating attached panels
if (isAttachedToNonFloating) {
var distance = (slideDirection === "left" || slideDirection === "right") ? width : height
return Math.round((1 - root.animationProgress) * distance)
}
// Small 40px slide for floating bar attached panels
if (isAttachedToFloatingBar) {
return (1 - root.animationProgress) * 40
}
// No slide for detached panels
return 0
}
// Animation properties
opacity: isAttachedToNonFloating ? Math.min(1, root.animationProgress * 5) : root.animationProgress
scale: {
if (root.disableScaleAnimation)
return 1
if (isAttachedToNonFloating)
return 1 // No scale for full slide animation
if (isAttachedToFloatingBar)
return 1 // No scale for floating bar (40px slide + opacity only)
return (0.9 + root.animationProgress * 0.1) // Scale for detached panels
}
// Transform origin for scale animation
transformOrigin: Item.Center
// Slide animation using transform
transform: Translate {
x: {
// Full slide from nearest edge for non-floating attached panels
if (panelBackground.isAttachedToNonFloating) {
if (panelBackground.slideDirection === "left")
return -panelBackground.slideOffset
if (panelBackground.slideDirection === "right")
return panelBackground.slideOffset
return 0
}
// Small 40px slide from bar for floating bar attached panels
if (panelBackground.isAttachedToFloatingBar) {
if (root.barPosition === "left")
return -panelBackground.slideOffset
if (root.barPosition === "right")
return panelBackground.slideOffset
}
return 0
}
y: {
// Full slide from nearest edge for non-floating attached panels
if (panelBackground.isAttachedToNonFloating) {
if (panelBackground.slideDirection === "top")
return -panelBackground.slideOffset
if (panelBackground.slideDirection === "bottom")
return panelBackground.slideOffset
return 0
}
// Small 40px slide from bar for floating bar attached panels
if (panelBackground.isAttachedToFloatingBar) {
if (root.barPosition === "top")
return -panelBackground.slideOffset
if (root.barPosition === "bottom")
return panelBackground.slideOffset
}
return 0
}
}
topLeftRadius: Style.radiusL
topRightRadius: Style.radiusL
bottomLeftRadius: Style.radiusL
bottomRightRadius: Style.radiusL
// Inverted corners based on bar attachment
// When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted
// Also invert corners when touching screen edges (non-floating bar only)
topLeftInverted: {
// Bar attachment: only attach to bar if bar opacity >= 1.0 (no color clash)
var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft))
// Also detect when panel touches bar edge (e.g., centered panel that's too tall)
var barTouchInverted = touchingTopBar || touchingLeftBar
// Screen edge contact: can attach to screen edges even if bar opacity < 1.0
// For horizontal bars: invert when touching left/right edges
// For vertical bars: invert when touching top/bottom edges
var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical))
// Also invert when touching screen edge opposite to bar (e.g., bottom edge when bar is at top)
var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
}
topRightInverted: {
var barInverted = couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight))
var barTouchInverted = touchingTopBar || touchingRightBar
var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical))
var oppositeEdgeInverted = couldAttach && (touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
}
bottomLeftInverted: {
var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && effectivePanelAnchorLeft))
var barTouchInverted = touchingBottomBar || touchingLeftBar
var edgeInverted = couldAttach && ((touchingLeftEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical))
var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
}
bottomRightInverted: {
var barInverted = couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && effectivePanelAnchorRight))
var barTouchInverted = touchingBottomBar || touchingRightBar
var edgeInverted = couldAttach && ((touchingRightEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical))
var oppositeEdgeInverted = couldAttach && (touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
return barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted
}
// Set inverted corner direction based on which edge touches
// Bar edges: horizontal bars → horizontal curves, vertical bars → vertical curves
// Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves
topLeftInvertedDirection: {
if (touchingLeftEdge && !root.barIsVertical)
return "vertical"
if (touchingTopEdge && root.barIsVertical)
return "horizontal"
return root.barIsVertical ? "vertical" : "horizontal"
}
topRightInvertedDirection: {
if (touchingRightEdge && !root.barIsVertical)
return "vertical"
if (touchingTopEdge && root.barIsVertical)
return "horizontal"
return root.barIsVertical ? "vertical" : "horizontal"
}
bottomLeftInvertedDirection: {
if (touchingLeftEdge && !root.barIsVertical)
return "vertical"
if (touchingBottomEdge && root.barIsVertical)
return "horizontal"
return root.barIsVertical ? "vertical" : "horizontal"
}
bottomRightInvertedDirection: {
if (touchingRightEdge && !root.barIsVertical)
return "vertical"
if (touchingBottomEdge && root.barIsVertical)
return "horizontal"
return root.barIsVertical ? "vertical" : "horizontal"
}
width: {
var w
// Priority 1: Content-driven size (dynamic)
if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) {
w = contentLoader.item.contentPreferredWidth
} // Priority 2: Ratio-based size
else if (root.preferredWidthRatio !== undefined) {
w = Math.round(Math.max((parent.width || 1920) * root.preferredWidthRatio, root.preferredWidth))
} // Priority 3: Static preferred width
else {
w = root.preferredWidth
}
return Math.min(w, (parent.width || 1920) - Style.marginL * 2)
}
height: {
var h
// Priority 1: Content-driven size (dynamic)
if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) {
h = contentLoader.item.contentPreferredHeight
} // Priority 2: Ratio-based size
else if (root.preferredHeightRatio !== undefined) {
h = Math.round(Math.max((parent.height || 1080) * root.preferredHeightRatio, root.preferredHeight))
} // Priority 3: Static preferred height
else {
h = root.preferredHeight
}
return Math.min(h, (parent.height || 1080) - Style.barHeight - Style.marginL * 2)
}
// Detect if panel is touching screen edges
readonly property bool touchingLeftEdge: couldAttach && x <= 1
readonly property bool touchingRightEdge: couldAttach && (x + width) >= (parent.width - 1)
readonly property bool touchingTopEdge: couldAttach && y <= 1
readonly property bool touchingBottomEdge: couldAttach && (y + height) >= (parent.height - 1)
// Detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints)
readonly property bool touchingTopBar: couldAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(y - (root.barMarginV + Style.barHeight)) <= 1
readonly property bool touchingBottomBar: couldAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((y + height) - (parent.height - root.barMarginV - Style.barHeight)) <= 1
readonly property bool touchingLeftBar: couldAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(x - (root.barMarginH + Style.barHeight)) <= 1
readonly property bool touchingRightBar: couldAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((x + width) - (parent.width - root.barMarginH - Style.barHeight)) <= 1
// Position the panel using explicit x/y coordinates (no anchors)
// This makes coordinates clearer for the click-through mask system
x: {
var calculatedX
// If useButtonPosition is enabled, align panel X with button
// Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid
// after the source panel (e.g., ControlCenter) closes, but we still have valid position data
if (root.useButtonPosition && parent.width > 0 && width > 0) {
if (root.barIsVertical) {
// For vertical bars
if (couldAttach) {
// Attached panels: align with bar edge (left or right side)
if (root.barPosition === "left") {
// Panel to the right of left bar
var leftBarEdge = root.barMarginH + Style.barHeight
// Panel sits right at bar edge (inverted corners align perfectly)
calculatedX = leftBarEdge
} else {
// right
// Panel to the left of right bar
var rightBarEdge = parent.width - root.barMarginH - Style.barHeight
// Panel sits right at bar edge (inverted corners align perfectly)
calculatedX = rightBarEdge - width
}
} else {
// Detached panels: center on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2
// Clamp to screen bounds with margins, accounting for bar position
var minX = Style.marginL
var maxX = parent.width - width - Style.marginL
// Account for vertical bar taking up space
if (root.barPosition === "left") {
minX = root.barMarginH + Style.barHeight + Style.marginL
} else if (root.barPosition === "right") {
maxX = parent.width - root.barMarginH - Style.barHeight - width - Style.marginL
}
panelX = Math.max(minX, Math.min(panelX, maxX))
calculatedX = panelX
}
} else {
// For horizontal bars, center panel on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2
// Clamp to bar bounds (account for floating bar margins)
// When attached, panel should not extend beyond bar edges
if (couldAttach) {
// Inverted corners with horizontal direction extend left/right by radiusL
// When bar is floating, it also has rounded corners, so we need extra insets
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = parent.width - root.barMarginH - cornerInset
panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width))
} else {
panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL))
}
calculatedX = panelX
}
} else {
// Standard anchor positioning
Logger.d("NPanel", "Fallback to standard anchor positioning")
if (root.panelAnchorHorizontalCenter) {
Logger.d("NPanel", " -> Horizontal center")
// Center horizontally, accounting for bar position and margins
if (root.barIsVertical) {
// For vertical bars, center in the available space not occupied by the bar
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = parent.width - availableStart
calculatedX = availableStart + (availableWidth - width) / 2
} else if (root.barPosition === "right") {
var availableWidth = parent.width - root.barMarginH - Style.barHeight
calculatedX = (availableWidth - width) / 2
} else {
// No vertical bar, center normally
calculatedX = (parent.width - width) / 2
}
} else {
// For horizontal bars or no bar, center normally
calculatedX = (parent.width - width) / 2
}
} else if (effectivePanelAnchorRight) {
Logger.d("NPanel", " -> Right anchor")
// When attached to right vertical bar, position next to bar (like useButtonPosition does)
if (couldAttach && root.barIsVertical && root.barPosition === "right") {
var rightBarEdge = parent.width - root.barMarginH - Style.barHeight
calculatedX = rightBarEdge - width
} else if (couldAttach) {
// Attach to right screen edge
calculatedX = parent.width - width
} else {
// Detached: use margin
calculatedX = parent.width - width - Style.marginL
}
} else if (effectivePanelAnchorLeft) {
Logger.d("NPanel", " -> Left anchor")
// When attached to left vertical bar, position next to bar (like useButtonPosition does)
if (couldAttach && root.barIsVertical && root.barPosition === "left") {
var leftBarEdge = root.barMarginH + Style.barHeight
calculatedX = leftBarEdge
} else if (couldAttach) {
// Attach to left screen edge
calculatedX = 0
} else {
// Detached: use margin
calculatedX = Style.marginL
}
} else {
// No explicit anchor: default to centering on bar
Logger.d("NPanel", " -> Default to center (no explicit anchor)")
// For horizontal bars: center horizontally
// For vertical bars: center horizontally in available space
if (root.barIsVertical) {
// Center in the space not occupied by the bar
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = parent.width - availableStart - Style.marginL
calculatedX = availableStart + (availableWidth - width) / 2
} else {
// right
var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL
calculatedX = Style.marginL + (availableWidth - width) / 2
}
} else {
// For horizontal bars: center horizontally, respect bar margins if attached
if (couldAttach) {
// When attached, respect bar bounds (like button position does)
var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0)
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = parent.width - root.barMarginH - cornerInset
var centeredX = (parent.width - width) / 2
calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width))
} else {
calculatedX = (parent.width - width) / 2
}
}
}
}
// Edge snapping: snap to screen edges if close (only when attached and bar is not floating)
if (couldAttach && !root.barFloating && parent.width > 0 && width > 0) {
// Calculate edge positions accounting for bar position
// For vertical bars (left/right), we need to position panels AFTER the bar, not behind it
var leftEdgePos = root.barMarginH
if (root.barPosition === "left") {
// Bar is on the left, so left edge is after the bar
leftEdgePos = root.barMarginH + Style.barHeight
}
var rightEdgePos = parent.width - root.barMarginH - width
if (root.barPosition === "right") {
// Bar is on the right, so right edge is before the bar
rightEdgePos = parent.width - root.barMarginH - Style.barHeight - width
}
// Snap to left edge if within snap distance
if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) {
calculatedX = leftEdgePos
} // Snap to right edge if within snap distance
else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) {
calculatedX = rightEdgePos
}
}
return calculatedX
}
y: {
var calculatedY
// If useButtonPosition is enabled, position panel relative to bar
// Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid
// after the source panel (e.g., ControlCenter) closes, but we still have valid position data
if (root.useButtonPosition && parent.height > 0 && height > 0) {
if (root.barPosition === "top") {
// Panel below top bar
var topBarEdge = root.barMarginV + Style.barHeight
if (couldAttach) {
// Panel sits right at bar edge (inverted corners align perfectly)
calculatedY = topBarEdge
} else {
calculatedY = topBarEdge + Style.marginM
}
} else if (root.barPosition === "bottom") {
// Panel above bottom bar
var bottomBarEdge = parent.height - root.barMarginV - Style.barHeight
if (couldAttach) {
// Panel sits right at bar edge (inverted corners align perfectly)
calculatedY = bottomBarEdge - height
} else {
calculatedY = bottomBarEdge - height - Style.marginM
}
} else if (root.barIsVertical) {
// For vertical bars, center panel on button Y position
var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2
// Clamp to bar bounds (account for floating bar margins and inverted corners)
var extraPadding = (couldAttach && root.barFloating) ? Style.radiusL : 0
if (couldAttach) {
// When attached, panel should not extend beyond bar edges (accounting for floating margins)
// Inverted corners with vertical direction extend up/down by radiusL
// When bar is floating, it also has rounded corners, so we need extra inset
var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0)
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = parent.height - root.barMarginV - cornerInset
panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - height))
} else {
panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding))
}
calculatedY = panelY
}
} else {
// Standard anchor positioning
// Calculate bar offset for detached panels - they should never overlap the bar
var barOffset = 0
if (!couldAttach) {
// For detached panels, always account for bar position
if (root.barPosition === "top") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
} else if (root.barPosition === "bottom") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
}
} else {
// For attached panels with explicit anchors
if (effectivePanelAnchorTop && root.barPosition === "top") {
// When attached to top bar: position right at bar edge (inverted corners align perfectly)
calculatedY = root.barMarginV + Style.barHeight
} else if (effectivePanelAnchorBottom && root.barPosition === "bottom") {
// When attached to bottom bar: position right at bar edge (inverted corners align perfectly)
calculatedY = parent.height - root.barMarginV - Style.barHeight - height
} else if (!root.hasExplicitVerticalAnchor) {
// No explicit vertical anchor AND attached: default to attaching to bar edge
if (root.barPosition === "top") {
// Attach to top bar
calculatedY = root.barMarginV + Style.barHeight
} else if (root.barPosition === "bottom") {
// Attach to bottom bar
calculatedY = parent.height - root.barMarginV - Style.barHeight - height
}
// For vertical bars with no explicit anchor: fall through to center vertically on bar
}
}
// Continue if calculatedY was already set above, or proceed with anchor positioning
if (calculatedY === undefined) {
if (root.panelAnchorVerticalCenter) {
// Center vertically, accounting for bar position and margins
if (!root.barIsVertical) {
// For horizontal bars, center in the available space not occupied by the bar
if (root.barPosition === "top") {
var availableStart = root.barMarginV + Style.barHeight
var availableHeight = parent.height - availableStart
calculatedY = availableStart + (availableHeight - height) / 2
} else if (root.barPosition === "bottom") {
var availableHeight = parent.height - root.barMarginV - Style.barHeight
calculatedY = (availableHeight - height) / 2
} else {
// No horizontal bar, center normally
calculatedY = (parent.height - height) / 2
}
} else {
// For vertical bars or no bar, center normally
calculatedY = (parent.height - height) / 2
}
} else if (effectivePanelAnchorTop) {
// When couldAttach=true, attach to top screen edge; otherwise use margin
if (couldAttach) {
calculatedY = 0
} else {
// Only apply barOffset if bar is also at top (to avoid overlapping)
var topBarOffset = (root.barPosition === "top") ? barOffset : 0
calculatedY = topBarOffset + Style.marginL
}
} else if (effectivePanelAnchorBottom) {
// When couldAttach=true, attach to bottom screen edge; otherwise use margin
if (couldAttach) {
calculatedY = parent.height - height
} else {
// Only apply barOffset if bar is also at bottom (to avoid overlapping)
var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0
calculatedY = parent.height - height - bottomBarOffset - Style.marginL
}
} else {
// No explicit vertical anchor
if (root.barIsVertical) {
// For vertical bars: center vertically on bar
if (couldAttach) {
// When attached, respect bar bounds
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = parent.height - root.barMarginV - cornerInset
var centeredY = (parent.height - height) / 2
calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height))
} else {
calculatedY = (parent.height - height) / 2
}
} else {
// For horizontal bars: attach to bar edge by default
if (couldAttach && !root.barIsVertical) {
if (root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
} else if (root.barPosition === "bottom") {
calculatedY = parent.height - root.barMarginV - Style.barHeight - height
}
} else {
// Detached or no bar position: use default positioning
if (root.barPosition === "top") {
calculatedY = barOffset + Style.marginL
} else if (root.barPosition === "bottom") {
calculatedY = Style.marginL
} else {
calculatedY = Style.marginL
}
}
}
}
}
}
// Edge snapping: snap to screen edges if close (only when attached and bar is not floating)
if (couldAttach && !root.barFloating && parent.height > 0 && height > 0) {
// Calculate edge positions accounting for bar position
// For horizontal bars (top/bottom), we need to position panels AFTER the bar, not behind it
var topEdgePos = root.barMarginV
if (root.barPosition === "top") {
// Bar is on the top, so top edge is after the bar
topEdgePos = root.barMarginV + Style.barHeight
}
var bottomEdgePos = parent.height - root.barMarginV - height
if (root.barPosition === "bottom") {
// Bar is on the bottom, so bottom edge is before the bar
bottomEdgePos = parent.height - root.barMarginV - Style.barHeight - height
}
// Snap to top edge if within snap distance
if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) {
calculatedY = topEdgePos
} // Snap to bottom edge if within snap distance
else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) {
calculatedY = bottomEdgePos
}
}
return calculatedY
}
// MouseArea to catch clicks on the panel and prevent them from reaching the background
// This prevents closing the panel when clicking inside it
MouseArea {
anchors.fill: parent
z: -1 // Behind content, but on the panel background
onClicked: {
// Accept and ignore - prevents propagation to background
}
}
// Panel content loader
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.panelContent
}
}
}
}
}
}