noctalia-shell/Widgets/NPanel.qml
LemmyCook cdca7c1d83 NPanel dimensions & Dimmer: Panels have no margin they are full screen and prevent clicking on the bar until dismissed.
Margins are now included in the rectangle X,Y coordinates calculation

Might sound weird at first but it fixes a lot of inconsistencies/issues
we have had for a long time when a panel was open:
- can't close panel when clicking in a dead zone of the bar.
- hovering an icon on the bar used to make it look like you could
interact with it, but the click would just close the panel and not
actuall y do anything with bar .

I recommend turning back on dimming, as it is now way cooler. Changed
the default to true.
2025-09-16 21:53:11 -04:00

403 lines
12 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
Loader {
id: root
property ShellScreen screen
property real scaling: 1.0
property Component panelContent: null
property real preferredWidth: 700
property real preferredHeight: 900
property real preferredWidthRatio
property real preferredHeightRatio
property color panelBackgroundColor: Color.mSurface
property bool panelAnchorHorizontalCenter: false
property bool panelAnchorVerticalCenter: false
property bool panelAnchorTop: false
property bool panelAnchorBottom: false
property bool panelAnchorLeft: false
property bool panelAnchorRight: false
// Properties to support positioning relative to the opener (button)
property bool useButtonPosition: false
property point buttonPosition: Qt.point(0, 0)
property int buttonWidth: 0
property int buttonHeight: 0
property bool panelKeyboardFocus: false
property bool backgroundClickEnabled: true
// Animation properties
readonly property real originalScale: 0.7
readonly property real originalOpacity: 0.0
property real scaleValue: originalScale
property real opacityValue: originalOpacity
property alias isClosing: hideTimer.running
signal opened
signal closed
active: false
asynchronous: true
Component.onCompleted: {
PanelService.registerPanel(root)
}
// -----------------------------------------
// Functions to control background click behavior
function disableBackgroundClick() {
backgroundClickEnabled = false
}
function enableBackgroundClick() {
// Add a small delay to prevent immediate close after drag release
enableBackgroundClickTimer.restart()
}
Timer {
id: enableBackgroundClickTimer
interval: 100
repeat: false
onTriggered: backgroundClickEnabled = true
}
// -----------------------------------------
function toggle(buttonItem) {
if (!active || isClosing) {
open(buttonItem)
} else {
close()
}
}
// -----------------------------------------
function open(buttonItem) {
// Get the button position if provided
if (buttonItem !== undefined && buttonItem !== null) {
useButtonPosition = true
var itemPos = buttonItem.mapToItem(null, 0, 0)
buttonPosition = Qt.point(itemPos.x, itemPos.y)
buttonWidth = buttonItem.width
buttonHeight = buttonItem.height
} else {
useButtonPosition = false
}
// Special case if currently closing/animating
if (isClosing) {
hideTimer.stop() // in case we were closing
scaleValue = 1.0
opacityValue = 1.0
}
PanelService.willOpenPanel(root)
backgroundClickEnabled = true
active = true
root.opened()
}
// -----------------------------------------
function close() {
scaleValue = originalScale
opacityValue = originalOpacity
hideTimer.start()
PanelService.willClosePanel(root)
}
// -----------------------------------------
function closeCompleted() {
root.closed()
active = false
useButtonPosition = false
backgroundClickEnabled = true
PanelService.closedPanel(root)
}
// -----------------------------------------
// Timer to disable the loader after the close animation is completed
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
closeCompleted()
}
}
// -----------------------------------------
sourceComponent: Component {
PanelWindow {
id: panelWindow
// PanelWindow has its own screen property inherited of QsWindow
property real scaling: ScalingService.getScreenScale(screen)
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0))
readonly property real verticalBarWidth: Math.round(Style.barHeight * scaling)
Component.onCompleted: {
Logger.log("NPanel", "Opened", root.objectName)
}
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
root.scaling = scaling = scale
}
}
}
Connections {
target: panelWindow
function onScreenChanged() {
root.screen = screen
root.scaling = scaling = ScalingService.getScreenScale(screen)
// It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched
panelContentLoader.active = false
panelContentLoader.active = true
}
}
visible: true
// No dimming here
color: Color.transparent
WlrLayershell.layer: Settings.data.general.dimDesktop ? WlrLayer.Overlay : WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-panel"
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
}
}
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
// Close any panel with Esc without requiring focus
Shortcut {
sequences: ["Escape"]
enabled: root.active && !root.isClosing
onActivated: root.close()
context: Qt.WindowShortcut
}
// Clicking outside of the rectangle to close
MouseArea {
anchors.fill: parent
enabled: root.backgroundClickEnabled
onClicked: root.close()
}
Rectangle {
id: panelBackground
color: panelBackgroundColor
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
width: {
var w
if (preferredWidthRatio !== undefined) {
w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) * scaling)
} else {
w = preferredWidth * scaling
}
// Clamp width so it is never bigger than the screen
return Math.min(w, screen?.width - Style.marginL * 2)
}
height: {
var h
if (preferredHeightRatio !== undefined) {
h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) * scaling)
} else {
h = preferredHeight * scaling
}
// Clamp width so it is never bigger than the screen
return Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
}
scale: root.scaleValue
opacity: root.opacityValue
x: calculatedX
y: calculatedY
// ---------------------------------------------
// Does not account for corners are they are negligible and helps keep the code clean.
// ---------------------------------------------
property real marginTop: {
if (!barIsVisible) {
return 0
}
switch (barPosition || panelAnchorVerticalCenter) {
case "top":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL * scaling : 0)
default:
return Style.marginS * scaling
}
}
property real marginBottom: {
if (!barIsVisible || panelAnchorVerticalCenter) {
return 0
}
switch (barPosition) {
case "bottom":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL * scaling : 0)
default:
return Style.marginS * scaling
}
}
property real marginLeft: {
if (!barIsVisible || panelAnchorHorizontalCenter) {
return 0
}
switch (barPosition) {
case "left":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL * scaling : 0)
default:
return Style.marginS * scaling
}
}
property real marginRight: {
if (!barIsVisible || panelAnchorHorizontalCenter) {
return 0
}
switch (barPosition) {
case "right":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL * scaling : 0)
default:
return Style.marginS * scaling
}
}
// ---------------------------------------------
property int calculatedX: {
// Priority to fixed anchoring
if (panelAnchorHorizontalCenter) {
return Math.round((panelWindow.width - panelBackground.width) / 2)
} else if (panelAnchorLeft) {
return marginLeft
} else if (panelAnchorRight) {
return Math.round(panelWindow.width - panelBackground.width - marginRight)
}
// No fixed anchoring
if (isVertical) {
// Vertical bar
if (barPosition === "right") {
// To the left of the right bar
return Math.round(panelWindow.width - panelBackground.width - marginRight)
} else {
// To the right of the left bar
return marginLeft
}
} else {
// Horizontal bar
if (root.useButtonPosition) {
// Position panel relative to button
var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2)
// Keep panel within screen bounds
var maxX = panelWindow.width - panelBackground.width - marginRight
var minX = marginLeft
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
} else {
// Fallback to center horizontally
return Math.round((panelWindow.width - panelBackground.width) / 2)
}
}
}
// ---------------------------------------------
property int calculatedY: {
// Priority to fixed anchoring
if (panelAnchorVerticalCenter) {
return Math.round((panelWindow.height - panelBackground.height) / 2)
} else if (panelAnchorTop) {
return marginTop
} else if (panelAnchorBottom) {
return Math.round(panelWindow.height - panelBackground.height - marginBottom)
}
// No fixed anchoring
if (isVertical) {
// Vertical bar
if (useButtonPosition) {
// Position panel relative to button
var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2)
// Keep panel within screen bounds
var maxY = panelWindow.height - panelBackground.height - marginBottom
var minY = marginTop
return Math.round(Math.max(minY, Math.min(targetY, maxY)))
} else {
// Fallback to center vertically
return Math.round((panelWindow.height - panelBackground.height) / 2)
}
} else {
// Horizontal bar
if (barPosition === "bottom") {
// Above the bottom bar
return Math.round(panelWindow.height - panelBackground.height - marginBottom)
} else {
// Below the top bar
return marginTop
}
}
}
// Animate in when component is completed
Component.onCompleted: {
root.scaleValue = 1.0
root.opacityValue = 1.0
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
Loader {
id: panelContentLoader
anchors.fill: parent
sourceComponent: root.panelContent
}
}
}
}
}