add noctalia and fuzzel
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+283
@@ -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
|
||||
}
|
||||
}
|
||||
+167
@@ -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)
|
||||
}
|
||||
}
|
||||
+74
@@ -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
Reference in New Issue
Block a user