This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -0,0 +1,164 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.UI
// ------------------------------
// MainScreen for each screen (manages bar + all panels)
// Wrapped in Loader to optimize memory - only loads when screen needs it
Variants {
model: Quickshell.screens
delegate: Item {
id: windowItem
required property ShellScreen modelData
property bool shouldBeActive: {
if (!modelData || !modelData.name) {
return false;
}
let shouldLoad = true;
if (!Settings.data.general.allowPanelsOnScreenWithoutBar) {
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
shouldLoad = monitors.length === 0 || monitors.includes(modelData?.name);
}
if (shouldLoad) {
Logger.d("AllScreens", "Screen activated: ", modelData?.name);
}
return shouldLoad;
}
property bool windowLoaded: false
// Main Screen loader - Bar and panels backgrounds
Loader {
id: windowLoader
active: parent.shouldBeActive
asynchronous: false
property ShellScreen loaderScreen: modelData
onLoaded: {
// Signal that window is loaded so exclusion zone can be created
parent.windowLoaded = true;
}
sourceComponent: MainScreen {
screen: windowLoader.loaderScreen
}
}
// Bar content in separate windows to prevent fullscreen redraws
// Note: Window stays alive when bar is hidden (visible=false) to avoid
// rapid Wayland surface destruction/creation that can crash compositors.
// Content is debounce-unloaded inside BarContentWindow.
Loader {
active: {
if (!parent.windowLoaded || !parent.shouldBeActive)
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
sourceComponent: BarContentWindow {
screen: modelData
}
onLoaded: {
Logger.d("AllScreens", "BarContentWindow created for", modelData?.name);
}
}
// BarTriggerZone - thin invisible zone to reveal hidden bar
// Always loaded when auto-hide is enabled (it's just 1px, no performance impact)
Loader {
active: {
if (!parent.windowLoaded || !parent.shouldBeActive)
return false;
if (!BarService.effectivelyVisible)
return false;
if (Settings.getBarDisplayModeForScreen(modelData?.name) !== "auto_hide")
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
sourceComponent: BarTriggerZone {
screen: modelData
}
onLoaded: {
Logger.d("AllScreens", "BarTriggerZone created for", modelData?.name);
}
}
// BarExclusionZone - created after MainScreen has fully loaded
// Note: Exclusion zone should NOT be affected by hideOnOverview setting.
// When bar is hidden during overview, the exclusion zone should remain to prevent
// windows from moving into the bar area. Auto-hide is handled by the component
// itself via ExclusionMode.Ignore/Auto.
Repeater {
model: Settings.data.bar.barType === "framed" ? ["top", "bottom", "left", "right"] : [Settings.getBarPositionForScreen(windowItem.modelData?.name)]
delegate: Loader {
active: {
if (!windowItem.windowLoaded || !windowItem.shouldBeActive)
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(windowItem.modelData?.name);
}
asynchronous: false
sourceComponent: BarExclusionZone {
screen: windowItem.modelData
edge: modelData
}
onLoaded: {
Logger.d("AllScreens", "BarExclusionZone (" + modelData + ") created for", windowItem.modelData?.name);
}
}
}
// PopupMenuWindow - reusable popup window for both tray menus and context menus
// Stays alive when bar is hidden to avoid Wayland surface churn crashes.
// PopupMenuWindow manages its own visibility internally.
Loader {
active: {
// Desktop widgets edit mode needs popup window on ALL screens
if (DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled) {
return true;
}
// Normal bar-based condition
if (!parent.windowLoaded || !parent.shouldBeActive)
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
sourceComponent: PopupMenuWindow {
screen: modelData
}
onLoaded: {
Logger.d("AllScreens", "PopupMenuWindow created for", modelData?.name);
}
}
}
}
@@ -0,0 +1,182 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Services.UI
import qs.Widgets
/**
* AllBackgrounds - Unified Shape container for all bar and panel backgrounds
*
* Unified shadow system. This component contains a single Shape
* with multiple ShapePath children (one for bar, one for each panel type).
*
* Benefits:
* - Single GPU-accelerated rendering pass for all backgrounds
* - Unified shadow system (one MultiEffect for everything)
*/
Item {
id: root
// Reference Bar
required property var bar
// Reference to MainScreen (for panel access)
required property var windowRoot
readonly property color panelBackgroundColor: Color.mSurface
anchors.fill: parent
// Unified background container
Item {
anchors.fill: parent
// When not using separate bar opacity, use unified approach (original behavior)
Item {
anchors.fill: parent
visible: !Settings.data.bar.useSeparateOpacity
// Enable layer caching to prevent continuous re-rendering
layer.enabled: true
opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity)
Shape {
id: unifiedBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
Component.onCompleted: {
Logger.d("AllBackgrounds", "AllBackgrounds initialized");
}
/**
* Bar
*/
BarBackground {
bar: root.bar
shapeContainer: unifiedBackgroundsShape
windowRoot: root.windowRoot
backgroundColor: panelBackgroundColor
}
/**
* Panel Background Slots
* Only 2 slots needed: one for currently open/opening panel, one for closing panel
*/
// Slot 0: Currently open/opening panel
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[0];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: unifiedBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
// Slot 1: Closing panel (during transitions)
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[1];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: unifiedBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
}
// Apply shadow to the unified backgrounds
NDropShadow {
anchors.fill: parent
source: unifiedBackgroundsShape
}
}
// When using separate bar opacity, separate the rendering
Item {
anchors.fill: parent
visible: Settings.data.bar.useSeparateOpacity
// Panel backgrounds with panel opacity
Item {
anchors.fill: parent
layer.enabled: true
opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity)
Shape {
id: panelBackgroundsShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
/**
* Panel Background Slots
* Only 2 slots needed: one for currently open/opening panel, one for closing panel
*/
// Slot 0: Currently open/opening panel
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[0];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: panelBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
// Slot 1: Closing panel (during transitions)
PanelBackground {
assignedPanel: {
var p = PanelService.backgroundSlotAssignments[1];
// Only render if this panel belongs to this screen
return (p && p.screen === root.windowRoot.screen) ? p : null;
}
shapeContainer: panelBackgroundsShape
defaultBackgroundColor: panelBackgroundColor
}
}
// Apply shadow to the panel backgrounds
NDropShadow {
anchors.fill: parent
source: panelBackgroundsShape
}
}
// Bar background with separate opacity
Item {
anchors.fill: parent
layer.enabled: true
opacity: Settings.data.bar.backgroundOpacity
Shape {
id: barBackgroundShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
BarBackground {
bar: root.bar
shapeContainer: barBackgroundShape
windowRoot: root.windowRoot
backgroundColor: panelBackgroundColor
}
}
NDropShadow {
anchors.fill: parent
source: barBackgroundShape
}
}
}
}
}
@@ -0,0 +1,283 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Modules.MainScreen.Backgrounds
import qs.Services.UI
/**
* BarBackground - ShapePath component for rendering the bar background
*
* Unified shadow system. This component is a ShapePath that will be
* a child of the unified AllBackgrounds Shape container.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
// Required reference to the bar component
required property var bar
// Required reference to AllBackgrounds shapeContainer
required property var shapeContainer
// Required reference to windowRoot for screen access
required property var windowRoot
required property color backgroundColor
// Check if bar should be visible on this screen
readonly property bool shouldShow: {
// Check global bar visibility (includes overview state)
if (!BarService.effectivelyVisible)
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || [];
var screenName = windowRoot?.screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName);
}
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// Framed bar properties
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property real frameRadius: Settings.data.bar.frameRadius ?? 20
// Bar position - since bar's parent fills the screen and Shape also fills the screen,
// we can use bar.x and bar.y directly (they're already in screen coordinates)
readonly property point barMappedPos: bar ? Qt.point(bar.x, bar.y) : Qt.point(0, 0)
// Effective dimensions - 0 when bar shouldn't show (similar to panel behavior)
readonly property real barWidth: (bar && shouldShow) ? bar.width : 0
readonly property real barHeight: (bar && shouldShow) ? bar.height : 0
// Screen dimensions for frame
readonly property real screenWidth: windowRoot?.screen?.width || 0
readonly property real screenHeight: windowRoot?.screen?.height || 0
// Inner hole dimensions for framed mode - always relative to screen
readonly property string barPosition: Settings.getBarPositionForScreen(windowRoot?.screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property real holeX: (barPosition === "left") ? barWidth : frameThickness
readonly property real holeY: (barPosition === "top") ? barHeight : frameThickness
readonly property real holeWidth: screenWidth - (barPosition === "left" || barPosition === "right" ? (barWidth + frameThickness) : (frameThickness * 2))
readonly property real holeHeight: screenHeight - (barPosition === "top" || barPosition === "bottom" ? (barHeight + frameThickness) : (frameThickness * 2))
// Flatten corners if bar is too small (handle null bar)
readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
readonly property real tlMultX: bar ? ShapeCornerHelper.getMultX(bar.topLeftCornerState) : 1
readonly property real tlMultY: bar ? ShapeCornerHelper.getMultY(bar.topLeftCornerState) : 1
readonly property real tlRadius: bar ? getCornerRadius(bar.topLeftCornerState) : 0
readonly property real trMultX: bar ? ShapeCornerHelper.getMultX(bar.topRightCornerState) : 1
readonly property real trMultY: bar ? ShapeCornerHelper.getMultY(bar.topRightCornerState) : 1
readonly property real trRadius: bar ? getCornerRadius(bar.topRightCornerState) : 0
readonly property real brMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomRightCornerState) : 1
readonly property real brMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomRightCornerState) : 1
readonly property real brRadius: bar ? getCornerRadius(bar.bottomRightCornerState) : 0
readonly property real blMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomLeftCornerState) : 1
readonly property real blMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomLeftCornerState) : 1
readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0
// True when the bar path has valid, non-degenerate geometry to render.
// Mirrors PanelBackground.isRenderable — prevents CurveRenderer crash on zero-area paths.
readonly property bool isRenderable: bar !== null && shouldShow && (isFramed ? (screenWidth > 0 && screenHeight > 0) : (barWidth > 0 && barHeight > 0))
// Edge overshoot: extend bar background beyond screen edges where both adjacent
// corners are flat (state -1) to prevent CurveRenderer antialiasing artifacts.
// Uses corner state checks instead of radius === 0 since flat corners now have _minR.
readonly property real screenEdgeOvershoot: 2
readonly property real topEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.topRightCornerState === -1 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && bar && bar.bottomLeftCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
readonly property real leftEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.bottomLeftCornerState === -1 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
readonly property real rightEdgeOvs: (!isFramed && shouldShow && bar && bar.topRightCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
// Auto-hide opacity factor for background fade
property real opacityFactor: (bar && bar.isHidden) ? 0 : 1
Behavior on opacityFactor {
enabled: bar && bar.autoHide
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
fillColor: isRenderable ? Qt.rgba(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a * opacityFactor) : "transparent"
fillRule: isFramed ? ShapePath.OddEvenFill : ShapePath.WindingFill
// Starting position — falls back to off-screen when not renderable so that
// all subsequent path elements form a valid non-degenerate off-screen square.
// Each edge is split between PathLine and PathArc so no arc has zero displacement,
// preventing CurveRenderer triangulation crashes on degenerate arcs.
// For framed mode the outer path is a full-screen rectangle; _minR offsets at each
// corner prevent zero-displacement zero-radius arcs that crash qTriangulate.
startX: isRenderable ? (isFramed ? _minR : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
startY: isRenderable ? (isFramed ? 0 : (barMappedPos.y + topEdgeOvs)) : -1
// ========== PATH DEFINITION ==========
// 1. Main Bar / Outer Screen Rectangle
// When !isRenderable all elements use fallback coordinates forming a valid 1×1
// off-screen square with non-degenerate arcs so CurveRenderer never receives
// a zero-area, bare-moveto, or zero-displacement arc path.
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
}
// Top-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
}
// Bottom-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
}
// Bottom-left corner
PathArc {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
}
// Top-left corner (back to start)
PathArc {
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
}
// 2. Inner Hole for Framed Mode (Clockwise)
// When !isFramed, draws a tiny 1x1 rectangle inside the bar as a non-degenerate WindingFill
// no-op to prevent a zero-area degenerate subpath crashing qTriangulate.
// Note: an exact duplicate of the outer path cannot be used here because Qt's CurveRenderer
// has issues with exactly coincident subpaths, causing the fill to not render on some systems.
// When !isRenderable, falls back to a valid 1×1 off-screen square at (-3,-3)→(-2,-2).
readonly property real _nhX: barMappedPos.x + barWidth / 2
readonly property real _nhY: barMappedPos.y + barHeight / 2
PathMove {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
}
// Top edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
}
// Top-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : (root._nhY + 0.25)) : -2.75
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Right edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 1)) : -2
}
// Bottom-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 0.75)) : -2.25
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Bottom edge
PathLine {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2
}
// Bottom-left corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 0.75)) : -2.25
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
// Left edge
PathLine {
x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3
y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : root._nhY) : -3
}
// Top-left corner (back to start)
PathArc {
x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75
y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3
radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0
direction: PathArc.Clockwise
}
}
@@ -0,0 +1,167 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
import qs.Modules.MainScreen.Backgrounds
/**
* PanelBackground - Dynamic ShapePath for rendering panel backgrounds
*
* Dynamically switches between panels based on which panel is currently
* assigned by PanelService. Only 2 instances are needed: one for the
* currently open panel and one for a closing panel during transitions.
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*/
ShapePath {
id: root
// Dynamically assigned panel (null if slot is unused)
property var assignedPanel: null
// Required reference to AllBackgrounds shapeContainer
required property var shapeContainer
// Default background color (used if panel doesn't specify one)
property color defaultBackgroundColor: Color.mSurface
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// Get panel's panelRegion (geometry placeholder)
readonly property var panelRegion: assignedPanel?.panelRegion ?? null
// Get the actual panelBackground Item from panelRegion
// Only access panelItem if panelRegion exists and is visible
readonly property var panelBg: (panelRegion && panelRegion.visible) ? panelRegion.panelItem : null
// Effective background color: use panel's if defined, else default
readonly property color effectiveBackgroundColor: {
if (!assignedPanel)
return "transparent";
if (assignedPanel.panelBackgroundColor !== undefined) {
return assignedPanel.panelBackgroundColor;
}
return defaultBackgroundColor;
}
// Panel position - panelBg is in screen coordinates already
readonly property real panelX: panelBg ? panelBg.x : 0
readonly property real panelY: panelBg ? panelBg.y : 0
readonly property real panelWidth: panelBg ? panelBg.width : 0
readonly property real panelHeight: panelBg ? panelBg.height : 0
readonly property bool isRenderable: assignedPanel && panelBg && panelWidth > 0 && panelHeight > 0
// Flatten corners if panel is too small
readonly property bool shouldFlatten: panelBg ? ShapeCornerHelper.shouldFlatten(panelWidth, panelHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? ShapeCornerHelper.getFlattenedRadius(Math.min(panelWidth, panelHeight), radius) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on panelBg's corner states
readonly property real tlMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topLeftCornerState) : 1
readonly property real tlMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topLeftCornerState) : 1
readonly property real tlRadius: panelBg ? getCornerRadius(panelBg.topLeftCornerState) : 0
readonly property real trMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topRightCornerState) : 1
readonly property real trMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topRightCornerState) : 1
readonly property real trRadius: panelBg ? getCornerRadius(panelBg.topRightCornerState) : 0
readonly property real brMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomRightCornerState) : 1
readonly property real brMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomRightCornerState) : 1
readonly property real brRadius: panelBg ? getCornerRadius(panelBg.bottomRightCornerState) : 0
readonly property real blMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomLeftCornerState) : 1
readonly property real blMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomLeftCornerState) : 1
readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
// Start point - use tiny off-screen non-degenerate fallback when not renderable.
// Fallback forms a 1×1 off-screen square where each edge is split between a PathLine
// and a PathArc, ensuring no arc has zero displacement (which can crash qTriangulate).
startX: isRenderable ? (panelX + tlRadius * tlMultX) : -0.75
startY: isRenderable ? panelY : -1
fillColor: isRenderable ? effectiveBackgroundColor : "transparent"
// ========== PATH DEFINITION ==========
// Draws a rectangle with potentially inverted corners
// All coordinates are relative to startX/startY
// Top edge (moving right)
PathLine {
relativeX: root.isRenderable ? (root.panelWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX) : 0.75
relativeY: 0
}
// Top-right corner arc
PathArc {
relativeX: root.isRenderable ? (root.trRadius * root.trMultX) : 0
relativeY: root.isRenderable ? (root.trRadius * root.trMultY) : 0.25
radiusX: root.isRenderable ? root.trRadius : 0
radiusY: root.isRenderable ? root.trRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: root.isRenderable ? (root.panelHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY) : 0.75
}
// Bottom-right corner arc
PathArc {
relativeX: root.isRenderable ? (-root.brRadius * root.brMultX) : -0.25
relativeY: root.isRenderable ? (root.brRadius * root.brMultY) : 0
radiusX: root.isRenderable ? root.brRadius : 0
radiusY: root.isRenderable ? root.brRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
}
// Bottom edge (moving left)
PathLine {
relativeX: root.isRenderable ? (-(root.panelWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX)) : -0.75
relativeY: 0
}
// Bottom-left corner arc
PathArc {
relativeX: root.isRenderable ? (-root.blRadius * root.blMultX) : 0
relativeY: root.isRenderable ? (-root.blRadius * root.blMultY) : -0.25
radiusX: root.isRenderable ? root.blRadius : 0
radiusY: root.isRenderable ? root.blRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
}
// Left edge (moving up) - closes the path back to start
PathLine {
relativeX: 0
relativeY: root.isRenderable ? (-(root.panelHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY)) : -0.75
}
// Top-left corner arc (back to start)
PathArc {
relativeX: root.isRenderable ? (root.tlRadius * root.tlMultX) : 0.25
relativeY: root.isRenderable ? (-root.tlRadius * root.tlMultY) : 0
radiusX: root.isRenderable ? root.tlRadius : 0
radiusY: root.isRenderable ? root.tlRadius : 0
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
}
}
@@ -0,0 +1,74 @@
pragma Singleton
import QtQuick
import QtQuick.Shapes
import Quickshell
/**
* ShapeCornerHelper - Utility singleton for shape corner calculations
*
* Uses 4-state per-corner system for flexible corner rendering:
* - State -1: No radius (flat/square corner)
* - State 0: Normal (inner curve)
* - State 1: Horizontal inversion (outer curve on X-axis)
* - State 2: Vertical inversion (outer curve on Y-axis)
*
* The key technique: Using PathArc direction control (Clockwise vs Counterclockwise)
* combined with multipliers to create both inner and outer corner curves.
*/
Singleton {
id: root
/**
* Get X-axis multiplier for a corner state
* State 1 (horizontal invert) returns -1, others return 1
*/
function getMultX(cornerState) {
return cornerState === 1 ? -1 : 1;
}
/**
* Get Y-axis multiplier for a corner state
* State 2 (vertical invert) returns -1, others return 1
*/
function getMultY(cornerState) {
return cornerState === 2 ? -1 : 1;
}
/**
* Get PathArc direction for a corner based on its multipliers
* Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise
* This creates the outer curve effect for inverted corners
*/
function getArcDirection(multX, multY) {
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise;
}
/**
* Convenience function to get arc direction directly from corner state
*/
function getArcDirectionFromState(cornerState) {
const multX = getMultX(cornerState);
const multY = getMultY(cornerState);
return getArcDirection(multX, multY);
}
/**
* Get the "flattening" radius when shape dimensions are too small
* Prevents visual artifacts when radius exceeds dimensions
*/
function getFlattenedRadius(dimension, requestedRadius) {
if (dimension < requestedRadius * 2) {
return dimension / 2;
}
return requestedRadius;
}
/**
* Check if a shape should use flattened corners
* Returns true if width or height is too small for the requested radius
*/
function shouldFlatten(width, height, radius) {
return width < radius * 2 || height < radius * 2;
}
}
@@ -0,0 +1,255 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Bar
import qs.Services.UI
/**
* BarContentWindow - Separate transparent PanelWindow for bar content
*
* This window contains only the bar widgets (content), while the background
* is rendered in MainScreen's unified Shape system. This separation prevents
* fullscreen redraws when bar widgets redraw.
*
* This component should be instantiated once per screen by AllScreens.qml
*/
PanelWindow {
id: barWindow
// Note: screen property is inherited from PanelWindow and should be set by parent
color: "transparent" // Transparent - background is in MainScreen below
// Window invisible when auto-hidden (blocks input) or toggled off via IPC.
// windowVisible stays true briefly after isHidden to allow fade-out animation.
property bool windowVisible: !isHidden
visible: contentLoaded && windowVisible && BarService.effectivelyVisible
Component.onCompleted: {
Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name);
if (!isHidden)
contentLoaded = true;
}
// Wayland layer configuration
WlrLayershell.namespace: "noctalia-bar-content-" + (barWindow.screen?.name || "unknown")
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone in MainScreen handles that
// Position and size to match bar location (per-screen)
readonly property string barPosition: Settings.getBarPositionForScreen(barWindow.screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property bool barFloating: Settings.data.bar.barType === "floating"
readonly property real barMarginH: Math.ceil(barFloating ? Settings.data.bar.marginHorizontal : 0)
readonly property real barMarginV: Math.ceil(barFloating ? Settings.data.bar.marginVertical : 0)
readonly property real barHeight: Style.getBarHeightForScreen(barWindow.screen?.name)
// Auto-hide properties
readonly property bool autoHide: Settings.getBarDisplayModeForScreen(barWindow.screen?.name) === "auto_hide"
readonly property int hideDelay: Settings.data.bar.autoHideDelay || 500
readonly property int showDelay: Settings.data.bar.autoShowDelay || 100
property bool isHidden: autoHide
// Hover tracking
property bool barHovered: false
// Check if any panel is open on this screen
readonly property bool panelOpen: PanelService.openedPanel !== null
// Timer for delayed hide
Timer {
id: hideTimer
interval: barWindow.hideDelay
onTriggered: {
if (barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen && !BarService.popupOpen) {
BarService.setScreenHidden(barWindow.screen?.name, true);
}
}
}
// Timer for delayed show
Timer {
id: showTimer
interval: barWindow.showDelay
onTriggered: {
// Only show if still hovered (via trigger zone or bar itself)
if (barWindow.autoHide && BarService.isBarHovered(barWindow.screen?.name)) {
BarService.setScreenHidden(barWindow.screen?.name, false);
}
}
}
// React to auto-hide state changes from BarService
Connections {
target: BarService
function onBarAutoHideStateChanged(screenName, hidden) {
Logger.d("BarContentWindow", "onBarAutoHideStateChanged:", screenName, hidden, "my screen:", barWindow.screen?.name);
if (screenName === barWindow.screen?.name) {
barWindow.isHidden = hidden;
}
}
function onBarHoverStateChanged(screenName, hovered) {
if (screenName === barWindow.screen?.name && barWindow.autoHide) {
if (hovered) {
hideTimer.stop();
// If bar is already visible, no need to delay
if (!barWindow.isHidden) {
showTimer.stop();
} else {
// Bar is hidden, use show delay
showTimer.restart();
}
} else if (!barWindow.barHovered && !barWindow.panelOpen) {
showTimer.stop();
hideTimer.restart();
}
}
}
}
// Don't hide when panel is open
onPanelOpenChanged: {
if (panelOpen && autoHide) {
hideTimer.stop();
BarService.setScreenHidden(barWindow.screen?.name, false);
} else if (!panelOpen && autoHide && !barHovered) {
hideTimer.restart();
}
}
// React to popup menu closing
Connections {
target: BarService
function onPopupOpenChanged() {
if (!BarService.popupOpen && barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen) {
hideTimer.restart();
}
}
}
// React to displayMode changes
onAutoHideChanged: {
if (!autoHide) {
// Show bar when auto-hide is disabled
hideTimer.stop();
showTimer.stop();
barWindow.isHidden = false;
}
// When auto-hide is enabled, don't immediately hide - wait for mouse to leave
}
// Anchor to the bar's edge
anchors {
top: barPosition === "top" || barIsVertical
bottom: barPosition === "bottom" || barIsVertical
left: barPosition === "left" || !barIsVertical
right: barPosition === "right" || !barIsVertical
}
// Content stays loaded once initialized — never unloaded during auto-hide.
// Destroying and recreating widgets on every hide/show cycle caused nested
// QML incubation crashes (SIGSEGV in QV4::Object::insertMember) because
// async widget Loaders complete during incubateFor() and their onLoaded
// handlers trigger signal cascades mid-incubation.
// The bar is hidden via opacity + window visibility instead.
property bool contentLoaded: false
// Delay window hide to allow fade-out animation to complete
Timer {
id: windowHideTimer
interval: Style.animationFast
onTriggered: {
if (barWindow.isHidden)
barWindow.windowVisible = false;
}
}
onIsHiddenChanged: {
if (isHidden) {
// Delay window hide so fade-out is visible
windowHideTimer.restart();
} else {
windowHideTimer.stop();
windowVisible = true;
if (!contentLoaded)
contentLoaded = true;
}
}
Connections {
target: BarService
function onEffectivelyVisibleChanged() {
if (BarService.effectivelyVisible && !barWindow.isHidden && !barWindow.contentLoaded) {
barWindow.contentLoaded = true;
}
}
}
// Handle floating margins and framed mode offsets
margins {
top: (barPosition === "top") ? barMarginV : (isFramed ? frameThickness : barMarginV)
bottom: (barPosition === "bottom") ? barMarginV : (isFramed ? frameThickness : barMarginV)
left: (barPosition === "left") ? barMarginH : (isFramed ? frameThickness : barMarginH)
right: (barPosition === "right") ? barMarginH : (isFramed ? frameThickness : barMarginH)
}
// Set a tight window size
implicitWidth: barIsVertical ? barHeight : barWindow.screen.width
implicitHeight: barIsVertical ? barWindow.screen.height : barHeight
// Bar content loader - loaded once, stays active for lifetime
Loader {
id: barLoader
anchors.fill: parent
active: barWindow.contentLoaded
sourceComponent: Item {
anchors.fill: parent
// Fade animation
opacity: barWindow.isHidden ? 0 : 1
Behavior on opacity {
enabled: barWindow.autoHide
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Bar {
id: barContent
anchors.fill: parent
screen: barWindow.screen
// Hover detection using HoverHandler (doesn't block child hover events)
HoverHandler {
id: hoverHandler
onHoveredChanged: {
if (hovered) {
barWindow.barHovered = true;
BarService.setScreenHovered(barWindow.screen?.name, true);
if (barWindow.autoHide) {
hideTimer.stop();
showTimer.restart();
}
} else {
// Skip if already hidden (being destroyed)
if (barWindow.isHidden)
return;
barWindow.barHovered = false;
BarService.setScreenHovered(barWindow.screen?.name, false);
if (barWindow.autoHide && !barWindow.panelOpen) {
showTimer.stop();
hideTimer.restart();
}
}
}
}
}
}
}
}
@@ -0,0 +1,74 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
/**
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
*
* This is a minimal window that works with the compositor to reserve space,
* while the actual bar UI is rendered in MainScreen.
*/
PanelWindow {
id: root
// Edge to anchor to and thickness to reserve
property string edge: Settings.getBarPositionForScreen(screen?.name)
property real thickness: (edge === Settings.getBarPositionForScreen(screen?.name)) ? Style.getBarHeightForScreen(screen?.name) : (Settings.data.bar.frameThickness ?? 12)
readonly property bool autoHide: Settings.getBarDisplayModeForScreen(screen?.name) === "auto_hide"
readonly property bool nonExclusive: Settings.getBarDisplayModeForScreen(screen?.name) === "non_exclusive"
readonly property bool barFloating: Settings.data.bar.barType === "floating"
readonly property real barMarginH: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginHorizontal) : 0
readonly property real barMarginV: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginVertical) : 0
// Allow users to enable a 1-physical-pixel inset for the exclusion zone so window borders can bleed under the bar
readonly property real bleedOffset: Settings.data.bar.enableExclusionZoneInset ? 1.0 : 0.0
readonly property real bleedInset: {
const info = CompositorService.displayScales[screen?.name];
const scale = (info && info.scale) ? info.scale : 1.0;
return bleedOffset / scale;
}
// Invisible - just reserves space
color: "transparent"
mask: Region {}
// Wayland layer shell configuration
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-bar-exclusion-" + edge + "-" + (screen?.name || "unknown")
// When auto-hide, non-exclusive mode is enabled, OR bar is explicitly hidden via IPC, don't reserve space
// Note: We check BarService.isVisible directly, NOT effectivelyVisible, because we want
// the exclusion zone to stay during overview (effectivelyVisible is false during overview
// when hideOnOverview is enabled, but isVisible remains true)
WlrLayershell.exclusionMode: (autoHide || nonExclusive || !BarService.isVisible) ? ExclusionMode.Ignore : ExclusionMode.Auto
// Anchor based on specified edge
anchors {
top: edge === "top"
bottom: edge === "bottom"
left: edge === "left" || edge === "top" || edge === "bottom"
right: edge === "right" || edge === "top" || edge === "bottom"
}
// Size based on orientation
implicitWidth: {
if (edge === "left" || edge === "right") {
return thickness + barMarginH - bleedInset;
}
return 0; // Auto-width when left/right anchors are true
}
implicitHeight: {
if (edge === "top" || edge === "bottom") {
return thickness + barMarginV - bleedInset;
}
return 0; // Auto-height when top/bottom anchors are true
}
Component.onCompleted: {
Logger.d("BarExclusionZone", "Created for screen:", screen?.name);
}
}
@@ -0,0 +1,66 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
/**
* BarTriggerZone - Thin invisible window at screen edge to reveal hidden bar
*
* This window is only active when the bar is in auto-hide mode and hidden.
* When the mouse enters this zone, it triggers the bar to show.
*/
PanelWindow {
id: root
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property int triggerSize: 1
// Track if component is being destroyed to prevent signals during cleanup
property bool isBeingDestroyed: false
Component.onDestruction: isBeingDestroyed = true
// Invisible trigger zone
color: "transparent"
focusable: false
WlrLayershell.namespace: "noctalia-bar-trigger-" + (screen?.name || "unknown")
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Anchor to bar's edge
anchors {
top: barPosition === "top" || barIsVertical
bottom: barPosition === "bottom" || barIsVertical
left: barPosition === "left" || !barIsVertical
right: barPosition === "right" || !barIsVertical
}
// Size based on orientation - thin strip at edge
implicitWidth: barIsVertical ? triggerSize : 0
implicitHeight: !barIsVertical ? triggerSize : 0
MouseArea {
id: triggerArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
if (root.isBeingDestroyed)
return;
// Signal hover - BarContentWindow will handle the show delay
BarService.setScreenHovered(root.screen?.name, true);
}
onExited: {
if (root.isBeingDestroyed)
return;
BarService.setScreenHovered(root.screen?.name, false);
}
}
Component.onCompleted: {
Logger.d("BarTriggerZone", "Created for screen:", screen?.name);
}
}
@@ -0,0 +1,690 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import "Backgrounds" as Backgrounds
import qs.Commons
// All panels
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Audio
import qs.Modules.Panels.Battery
import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Brightness
import qs.Modules.Panels.Changelog
import qs.Modules.Panels.Clock
import qs.Modules.Panels.ControlCenter
import qs.Modules.Panels.Dock
import qs.Modules.Panels.Launcher
import qs.Modules.Panels.Media
import qs.Modules.Panels.Network
import qs.Modules.Panels.NotificationHistory
import qs.Modules.Panels.Plugins
import qs.Modules.Panels.SessionMenu
import qs.Modules.Panels.Settings
import qs.Modules.Panels.SetupWizard
import qs.Modules.Panels.SystemStats
import qs.Modules.Panels.Tray
import qs.Modules.Panels.Wallpaper
import qs.Services.Compositor
import qs.Services.Power
import qs.Services.UI
/**
* MainScreen - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
Component.onCompleted: {
Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y);
}
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-background-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that
WlrLayershell.keyboardFocus: {
// No panel open anywhere: no keyboard focus needed
if (!root.isAnyPanelOpen) {
return WlrKeyboardFocus.None;
}
// Panel open on THIS screen: use panel's preferred focus mode
if (root.isPanelOpen) {
// Hyprland's Exclusive captures ALL input globally (including pointer),
// preventing click-to-close from working on other monitors.
// Workaround: briefly use Exclusive when panel opens (for text input focus),
// then switch to OnDemand (for click-to-close on other screens).
if (CompositorService.isHyprland) {
return PanelService.isInitializingKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand;
}
return PanelService.openedPanel.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand;
}
// Panel open on ANOTHER screen: OnDemand allows receiving pointer events for click-to-close
return WlrKeyboardFocus.OnDemand;
}
anchors {
top: true
bottom: true
left: true
right: true
}
// Desktop dimming when panels are open
property real dimmerOpacity: Settings.data.general.dimmerOpacity ?? 0.8
property bool isPanelOpen: (PanelService.openedPanel !== null) && (PanelService.openedPanel.screen === screen)
property bool isPanelClosing: (PanelService.openedPanel !== null) && PanelService.openedPanel.isClosing
property bool isAnyPanelOpen: PanelService.openedPanel !== null
color: {
if (dimmerOpacity > 0 && isPanelOpen && !isPanelClosing) {
return Qt.alpha(Color.mShadow, dimmerOpacity);
}
return "transparent";
}
Behavior on color {
enabled: !PanelService.closedImmediately
ColorAnimation {
duration: isPanelClosing ? Style.animationFaster : Style.animationNormal
easing.type: Easing.OutQuad
}
}
// Reset closedImmediately flag after color change is applied
onColorChanged: {
if (PanelService.closedImmediately) {
PanelService.closedImmediately = false;
}
}
// Check if bar should be visible on this screen
readonly property bool barShouldShow: {
// Check global bar visibility (includes overview state)
if (!BarService.effectivelyVisible)
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || [];
var screenName = screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName);
}
// Make everything click-through except bar
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
// Only include regions that are actually needed
// panelRegions is handled by PanelService, bar is local to this screen
regions: [barMaskRegion, backgroundMaskRegion]
// Bar region - subtract bar area from mask (only if bar should be shown on this screen)
Region {
id: barMaskRegion
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real barThickness: Style.barHeight
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property string barPos: Settings.data.bar.position || "top"
// Bar / Frame Mask
Region {
// Mode: Simple or Floating
x: barPlaceholder.x
y: barPlaceholder.y
width: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.width : 0
height: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.height : 0
intersection: Intersection.Subtract
}
// Mode: Framed - 4 sides
Region {
// Top side
Region {
x: 0
y: 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "top" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
intersection: Intersection.Subtract
}
// Bottom side
Region {
x: 0
y: (barMaskRegion.isFramed && root.barShouldShow) ? (root.height - (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
intersection: Intersection.Subtract
}
// Left side
Region {
x: 0
y: 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "left" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0
intersection: Intersection.Subtract
}
// Right side
Region {
x: (barMaskRegion.isFramed && root.barShouldShow) ? (root.width - (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0
intersection: Intersection.Subtract
}
}
}
// Background region for click-to-close - reactive sizing
// Uses isAnyPanelOpen so clicking on any screen's background closes the panel
Region {
id: backgroundMaskRegion
x: 0
y: 0
width: root.isAnyPanelOpen ? root.width : 0
height: root.isAnyPanelOpen ? root.height : 0
intersection: Intersection.Subtract
}
}
// Blur behind the bar and open panels — attached to PanelWindow (required by BackgroundEffect API)
BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? blurRegion : null
Region {
id: blurRegion
// ── Non-framed bar (simple/floating): single rectangle with bar corner states ──
Region {
x: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.x : 0
y: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.y : 0
width: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.width : 0
height: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.height : 0
radius: Style.radiusL
topLeftCorner: barPlaceholder.topLeftCornerState
topRightCorner: barPlaceholder.topRightCornerState
bottomLeftCorner: barPlaceholder.bottomLeftCornerState
bottomRightCorner: barPlaceholder.bottomRightCornerState
}
// ── Framed bar: full screen minus rounded hole ──
Region {
x: 0
y: 0
width: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.width : 0
height: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.height : 0
Region {
intersection: Intersection.Subtract
x: backgroundBlur.frameHoleX
y: backgroundBlur.frameHoleY
width: backgroundBlur.frameHoleX2 - backgroundBlur.frameHoleX
height: backgroundBlur.frameHoleY2 - backgroundBlur.frameHoleY
radius: backgroundBlur.frameR
}
}
// ── Panel blur regions ──
// Opening panel
Region {
x: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.x) : 0
y: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.y) : 0
width: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.width) : 0
height: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.height) : 0
radius: Style.radiusL
topLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topLeftCornerState : CornerState.Normal
topRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topRightCornerState : CornerState.Normal
bottomLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomLeftCornerState : CornerState.Normal
bottomRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomRightCornerState : CornerState.Normal
}
// Closing panel (coexists with opening panel during transition)
Region {
x: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.x) : 0
y: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.y) : 0
width: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.width) : 0
height: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.height) : 0
radius: Style.radiusL
topLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topLeftCornerState : CornerState.Normal
topRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topRightCornerState : CornerState.Normal
bottomLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomLeftCornerState : CornerState.Normal
bottomRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomRightCornerState : CornerState.Normal
}
}
// --------------------------------------
// Container for all UI elements
Item {
id: container
width: root.width
height: root.height
// Unified backgrounds container / unified shadow system
// Renders all bar and panel backgrounds as ShapePaths within a single Shape
// This allows the shadow effect to apply to all backgrounds in one render pass
Backgrounds.AllBackgrounds {
id: unifiedBackgrounds
anchors.fill: parent
bar: barPlaceholder.barItem || null
windowRoot: root
z: 0 // Behind all content
}
// Background MouseArea for closing panels when clicking outside
// Uses isAnyPanelOpen so clicking on any screen's background closes the panel
MouseArea {
anchors.fill: parent
enabled: root.isAnyPanelOpen
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (PanelService.openedPanel) {
PanelService.openedPanel.close();
}
}
z: 0 // Behind panels and bar
}
// ---------------------------------------
// All panels always exist
// ---------------------------------------
AudioPanel {
id: audioPanel
objectName: "audioPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
MediaPlayerPanel {
id: mediaPlayerPanel
objectName: "mediaPlayerPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BatteryPanel {
id: batteryPanel
objectName: "batteryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BluetoothPanel {
id: bluetoothPanel
objectName: "bluetoothPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BrightnessPanel {
id: brightnessPanel
objectName: "brightnessPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ControlCenterPanel {
id: controlCenterPanel
objectName: "controlCenterPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ChangelogPanel {
id: changelogPanel
objectName: "changelogPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ClockPanel {
id: clockPanel
objectName: "clockPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
Launcher {
id: launcherPanel
objectName: "launcherPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
NotificationHistoryPanel {
id: notificationHistoryPanel
objectName: "notificationHistoryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SessionMenu {
id: sessionMenuPanel
objectName: "sessionMenuPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SettingsPanel {
id: settingsPanel
objectName: "settingsPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SetupWizard {
id: setupWizardPanel
objectName: "setupWizardPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
TrayDrawerPanel {
id: trayDrawerPanel
objectName: "trayDrawerPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
NetworkPanel {
id: networkPanel
objectName: "networkPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SystemStatsPanel {
id: systemStatsPanel
objectName: "systemStatsPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
StaticDockPanel {
id: staticDockPanel
objectName: "staticDockPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
// ----------------------------------------------
// Plugin panel slots
// ----------------------------------------------
PluginPanelSlot {
id: pluginPanel1
objectName: "pluginPanel1-" + (root.screen?.name || "unknown")
screen: root.screen
slotNumber: 1
}
PluginPanelSlot {
id: pluginPanel2
objectName: "pluginPanel2-" + (root.screen?.name || "unknown")
screen: root.screen
slotNumber: 2
}
// ----------------------------------------------
// Bar background placeholder - just for background positioning (actual bar content is in BarContentWindow)
Item {
id: barPlaceholder
// Expose self as barItem for AllBackgrounds compatibility
readonly property var barItem: barPlaceholder
// Screen reference
property ShellScreen screen: root.screen
// Bar background positioning properties (per-screen)
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property bool barFloating: Settings.data.bar.barType === "floating"
readonly property real barMarginH: barFloating ? Math.floor(Settings.data.bar.marginHorizontal) : 0
readonly property real barMarginV: barFloating ? Math.floor(Settings.data.bar.marginVertical) : 0
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
// Auto-hide properties (read by AllBackgrounds for background fade)
readonly property bool autoHide: Settings.getBarDisplayModeForScreen(screen?.name) === "auto_hide"
property bool isHidden: autoHide
Connections {
target: BarService
function onBarAutoHideStateChanged(screenName, hidden) {
if (screenName === barPlaceholder.screen?.name) {
barPlaceholder.isHidden = hidden;
}
}
}
// Expose bar dimensions directly on this Item for BarBackground
// Use screen dimensions directly
x: {
if (barPosition === "right")
return (screen?.width ?? 0) - barHeight - barMarginH;
if (isFramed && !barIsVertical)
return frameThickness;
return barMarginH;
}
y: {
if (barPosition === "bottom")
return (screen?.height ?? 0) - barHeight - barMarginV;
if (isFramed && barIsVertical)
return frameThickness;
return barMarginV;
}
width: {
if (barIsVertical) {
return barHeight;
}
if (isFramed)
return (screen?.width ?? 0) - frameThickness * 2;
return (screen?.width ?? 0) - barMarginH * 2;
}
height: {
if (!barIsVertical) {
return barHeight;
}
if (isFramed)
return (screen?.height ?? 0) - frameThickness * 2;
return (screen?.height ?? 0) - barMarginV * 2;
}
// Corner states (same as Bar.qml)
readonly property int topLeftCornerState: {
if (barFloating)
return 0;
if (barPosition === "top")
return -1;
if (barPosition === "left")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int topRightCornerState: {
if (barFloating)
return 0;
if (barPosition === "top")
return -1;
if (barPosition === "right")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int bottomLeftCornerState: {
if (barFloating)
return 0;
if (barPosition === "bottom")
return -1;
if (barPosition === "left")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int bottomRightCornerState: {
if (barFloating)
return 0;
if (barPosition === "bottom")
return -1;
if (barPosition === "right")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
}
// Screen Corners
ScreenCorners {}
// Blur behind the bar and open panels
// Helper object holding computed properties for blur regions
QtObject {
id: backgroundBlur
// Panel background geometry (from the currently open panel on this screen)
readonly property var panelBg: {
var op = PanelService.openedPanel;
if (!op || op.screen !== root.screen || op.blurEnabled === false)
return null;
var region = op.panelRegion;
return (region && region.visible) ? region.panelItem : null;
}
// Panel background geometry for the closing panel (may coexist with panelBg)
readonly property var closingPanelBg: {
var cp = PanelService.closingPanel;
if (!cp || cp.screen !== root.screen || cp.blurEnabled === false)
return null;
var region = cp.panelRegion;
return (region && region.visible) ? region.panelItem : null;
}
// Framed bar: inner hole boundary (where the hole begins on each axis)
// These are the x/y coordinates of the 4 inner hole corners
readonly property real frameHoleX: barPlaceholder.barPosition === "left" ? barPlaceholder.barHeight : barPlaceholder.frameThickness
readonly property real frameHoleY: barPlaceholder.barPosition === "top" ? barPlaceholder.barHeight : barPlaceholder.frameThickness
readonly property real frameHoleX2: root.width - (barPlaceholder.barPosition === "right" ? barPlaceholder.barHeight : barPlaceholder.frameThickness)
readonly property real frameHoleY2: root.height - (barPlaceholder.barPosition === "bottom" ? barPlaceholder.barHeight : barPlaceholder.frameThickness)
readonly property real frameR: Settings.data.bar.frameRadius ?? 20
}
// Native idle inhibitor — one per active MainScreen window.
// Multiple inhibitors bound to the same enabled state are harmless;
// having one per screen is more robust than picking a "primary" screen.
IdleInhibitor {
window: root
enabled: IdleInhibitorService.isInhibited
Component.onCompleted: {
IdleInhibitorService.nativeInhibitorAvailable = true;
Logger.d("IdleInhibitor", "Native IdleInhibitor active on screen:", root.screen?.name);
}
}
}
// Centralized Keyboard Shortcuts
// These shortcuts delegate to the opened panel's handler functions
// Panels can implement: onEscapePressed, onTabPressed, onBackTabPressed,
// onUpPressed, onDownPressed, onReturnPressed, etc...
Instantiator {
model: Settings.data.general.keybinds.keyEscape || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onEscapePressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onEscapePressed()
}
}
Shortcut {
sequence: "Tab"
enabled: root.isPanelOpen && (PanelService.openedPanel.onTabPressed !== undefined)
onActivated: PanelService.openedPanel.onTabPressed()
}
Shortcut {
sequence: "Backtab"
enabled: root.isPanelOpen && (PanelService.openedPanel.onBackTabPressed !== undefined)
onActivated: PanelService.openedPanel.onBackTabPressed()
}
Instantiator {
model: Settings.data.general.keybinds.keyUp || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onUpPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onUpPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyDown || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onDownPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onDownPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyEnter || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onEnterPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onEnterPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyLeft || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onLeftPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onLeftPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyRight || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onRightPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onRightPressed()
}
}
Shortcut {
sequence: "Home"
enabled: root.isPanelOpen && (PanelService.openedPanel.onHomePressed !== undefined)
onActivated: PanelService.openedPanel.onHomePressed()
}
Shortcut {
sequence: "End"
enabled: root.isPanelOpen && (PanelService.openedPanel.onEndPressed !== undefined)
onActivated: PanelService.openedPanel.onEndPressed()
}
Shortcut {
sequence: "PgUp"
enabled: root.isPanelOpen && (PanelService.openedPanel.onPageUpPressed !== undefined)
onActivated: PanelService.openedPanel.onPageUpPressed()
}
Shortcut {
sequence: "PgDown"
enabled: root.isPanelOpen && (PanelService.openedPanel.onPageDownPressed !== undefined)
onActivated: PanelService.openedPanel.onPageDownPressed()
}
}
@@ -0,0 +1,167 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
// Generic full-screen popup window for menus and context menus
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
// Provides click-outside-to-close functionality for any popup content
// Loads TrayMenu by default but can show context menus via showContextMenu()
PanelWindow {
id: root
required property ShellScreen screen
property string windowType: "popupmenu" // Used for namespace and registration
// Content item to display (set by the popup that uses this window)
property var contentItem: null
// Expose the trayMenu Loader directly (for backward compatibility)
readonly property alias trayMenuLoader: trayMenuLoader
// Dynamic context menu callback for items in other windows (e.g., desktop widgets)
property var dynamicMenuCallback: null
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: "transparent"
// Use Top layer for proper event handling, but on labwc use Bottom
// to avoid stealing input from popups while still catching outside clicks.
// However, when a dialog is open, always use Top so dialogs appear above apps.
WlrLayershell.layer: (CompositorService.isLabwc && !hasDialog) ? WlrLayer.Bottom : WlrLayer.Top
WlrLayershell.keyboardFocus: hasDialog ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Track if a dialog is currently open (needed for keyboard focus)
property bool hasDialog: false
// Register with PanelService so widgets can find this window
Component.onCompleted: {
objectName = "popupMenuWindow-" + (screen?.name || "unknown");
PanelService.registerPopupMenuWindow(screen, root);
}
Component.onDestruction: {
PanelService.unregisterPopupMenuWindow(screen);
}
// Load TrayMenu as the default content
Loader {
id: trayMenuLoader
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
onLoaded: {
if (item) {
item.screen = root.screen;
// Set the loaded item as default content
root.contentItem = item;
}
}
}
// Dynamic context menu - created as child of this window (Top layer) so input works correctly
// Used for items in other windows like desktop widgets (bottom layer)
NPopupContextMenu {
id: dynamicMenu
visible: false
screen: root.screen
minWidth: 180
onTriggered: (action, item) => {
if (root.dynamicMenuCallback) {
// Callback returns true if it will handle closing (e.g., opening a dialog)
var handled = root.dynamicMenuCallback(action);
if (!handled) {
root.close();
}
} else {
root.close();
}
}
}
function open() {
visible = true;
BarService.popupOpen = true;
}
// Show a context menu (temporarily replaces TrayMenu as content)
function showContextMenu(menu) {
if (menu) {
contentItem = menu;
open();
}
}
// Show a dynamic context menu with model and callback at screen coordinates
// Used for items in other window layers (e.g., desktop widgets in bottom layer)
function showDynamicContextMenu(model, screenX, screenY, callback) {
dynamicMenu.model = model;
dynamicMenuCallback = callback;
// Use the anchor point item for positioning at absolute coordinates
dynamicMenu.anchorItem = anchorPoint;
anchorPoint.x = screenX;
anchorPoint.y = screenY;
contentItem = dynamicMenu;
dynamicMenu.visible = true;
open();
}
// Invisible anchor point for dynamic menu positioning
Item {
id: anchorPoint
width: 1
height: 1
}
// Hide just the dynamic menu without closing the popup window
// Used when transitioning from context menu to a dialog
function hideDynamicMenu() {
dynamicMenu.visible = false;
}
function close() {
visible = false;
BarService.popupOpen = false;
// Call close/hide method on current content
if (contentItem) {
if (typeof contentItem.hideMenu === "function") {
contentItem.hideMenu();
} else if (typeof contentItem.close === "function") {
contentItem.close();
}
}
// Hide dynamic menu
dynamicMenu.visible = false;
dynamicMenuCallback = null;
// Restore TrayMenu as default content
if (trayMenuLoader.item) {
contentItem = trayMenuLoader.item;
}
}
// Full-screen click catcher - click anywhere outside content closes the window
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.close()
}
// Container for dialogs that need a full-screen Item parent (e.g., Qt Popup)
Item {
id: dialogContainer
anchors.fill: parent
}
// Expose the dialog container for external use
readonly property alias dialogParent: dialogContainer
}
@@ -0,0 +1,231 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
/**
* ScreenCorners - Shape component for rendering screen corners
*
* Renders concave corners at the screen edges to create a rounded screen effect.
* Self-contained Shape component (no shadows).
*/
Item {
id: root
anchors.fill: parent
// Wrapper with layer caching to reduce GPU tessellation overhead
Item {
anchors.fill: parent
// Cache the Shape to a texture to prevent continuous re-tessellation
layer.enabled: true
Shape {
id: cornersShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false // Disable mouse input
visible: cornersPath.cornerRadius > 0 && width > 0 && height > 0
ShapePath {
id: cornersPath
// Corner configuration
readonly property color cornerColor: Settings.data.general.forceBlackScreenCorners ? "black" : Color.mSurface
readonly property real cornerRadius: Style.screenRadius
readonly property real cornerSize: Style.screenRadius
// Determine margins based on bar position
readonly property real topMargin: 0
readonly property real bottomMargin: 0
readonly property real leftMargin: 0
readonly property real rightMargin: 0
// Screen dimensions
readonly property real screenWidth: cornersShape.width
readonly property real screenHeight: cornersShape.height
// Only show screen corners if enabled and appropriate conditions are met
readonly property bool shouldShow: Settings.data.general.showScreenCorners
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
fillColor: shouldShow ? cornerColor : "transparent"
// Smooth color animation (disabled during theme transitions to sync with Color.qml)
Behavior on fillColor {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
}
}
// ========== PATH DEFINITION ==========
// Draws 4 separate corner squares at screen edges
// Each corner square has a concave arc on the inner diagonal
// ========== TOP-LEFT CORNER ==========
// Arc is at the bottom-right of this square (inner diagonal)
// Start at top-left screen corner
startX: leftMargin
startY: topMargin
// Top edge (moving right)
PathLine {
relativeX: cornersPath.cornerSize
relativeY: 0
}
// Right edge (moving down toward arc)
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
}
// Concave arc (bottom-right corner of square, curving inward toward screen center)
PathArc {
relativeX: -cornersPath.cornerRadius
relativeY: cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
// Bottom edge (moving left)
PathLine {
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
relativeY: 0
}
// Left edge (moving up) - closes back to start
PathLine {
relativeX: 0
relativeY: -cornersPath.cornerSize
}
// ========== TOP-RIGHT CORNER ==========
// Arc is at the bottom-left of this square (inner diagonal)
PathMove {
x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize
y: cornersPath.topMargin
}
// Top edge (moving right)
PathLine {
relativeX: cornersPath.cornerSize
relativeY: 0
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize
}
// Bottom edge (moving left toward arc)
PathLine {
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
relativeY: 0
}
// Concave arc (bottom-left corner of square, curving inward toward screen center)
PathArc {
relativeX: -cornersPath.cornerRadius
relativeY: -cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
// Left edge (moving up) - closes back to start
PathLine {
relativeX: 0
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
}
// ========== BOTTOM-LEFT CORNER ==========
// Arc is at the top-right of this square (inner diagonal)
PathMove {
x: cornersPath.leftMargin
y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize
}
// Top edge (moving right toward arc)
PathLine {
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
relativeY: 0
}
// Concave arc (top-right corner of square, curving inward toward screen center)
PathArc {
relativeX: cornersPath.cornerRadius
relativeY: cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
}
// Bottom edge (moving left)
PathLine {
relativeX: -cornersPath.cornerSize
relativeY: 0
}
// Left edge (moving up) - closes back to start
PathLine {
relativeX: 0
relativeY: -cornersPath.cornerSize
}
// ========== BOTTOM-RIGHT CORNER ==========
// Arc is at the top-left of this square (inner diagonal)
// Start at bottom-right of square (different from other corners!)
PathMove {
x: cornersPath.screenWidth - cornersPath.rightMargin
y: cornersPath.screenHeight - cornersPath.bottomMargin
}
// Bottom edge (moving left)
PathLine {
relativeX: -cornersPath.cornerSize
relativeY: 0
}
// Left edge (moving up toward arc)
PathLine {
relativeX: 0
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
}
// Concave arc (top-left corner of square, curving inward toward screen center)
PathArc {
relativeX: cornersPath.cornerRadius
relativeY: -cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
// Top edge (moving right)
PathLine {
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
relativeY: 0
}
// Right edge (moving down) - closes back to start
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize
}
}
}
}
}
File diff suppressed because it is too large Load Diff