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

This commit is contained in:
ItsLemmy
2025-11-06 10:41:48 -05:00
parent 76a182a90c
commit e29c6ee1a6
109 changed files with 2381 additions and 2192 deletions
+1 -1
View File
@@ -279,7 +279,7 @@ Singleton {
interpolations = {}
if (!isLoaded) {
Logger.d("I18n", "Translations not loaded yet")
//Logger.d("I18n", "Translations not loaded yet")
return key
}
+79 -20
View File
@@ -14,12 +14,15 @@ import qs.Modules.Bar.Extras
Item {
id: root
// This property will be set by NFullScreenWindow
// This property will be set by MainScreen
property ShellScreen screen: null
// Expose bar region for click-through mask
readonly property var barRegion: barContentLoader.item?.children[0] || null
// Expose the actual bar Item for unified background system
readonly property var barItem: barRegion
// Bar positioning properties
readonly property string barPosition: Settings.data.bar.position || "top"
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
@@ -61,8 +64,8 @@ Item {
sourceComponent: Item {
anchors.fill: parent
// Background fill
NShapedRectangle {
// Bar container (background rendered by AllBackgrounds)
Item {
id: bar
// Position and size the bar based on orientation and floating margins
@@ -92,26 +95,82 @@ Item {
return baseHeight // Vertical bars extend via width, not height
}
backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
// Corner states for new unified background system
// 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)
readonly property int topLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
// Left bar: top-left against screen edge = no radius
if (barPosition === "left")
return -1
// Bottom/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2 // horizontal invert for vertical bars, vertical invert for horizontal
}
// No outerCorners = square
return -1
}
// Floating bar rounded corners
topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0
topRightRadius: Settings.data.bar.floating || topRightInverted ? Style.radiusL : 0
bottomLeftRadius: Settings.data.bar.floating || bottomLeftInverted ? Style.radiusL : 0
bottomRightRadius: Settings.data.bar.floating || bottomRightInverted ? Style.radiusL : 0
readonly property int topRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Top bar: top corners against screen edge = no radius
if (barPosition === "top")
return -1
// Right bar: top-right against screen edge = no radius
if (barPosition === "right")
return -1
// Bottom/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")
topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical"
topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")
topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical"
readonly property int bottomLeftCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
// Left bar: bottom-left against screen edge = no radius
if (barPosition === "left")
return -1
// Top/Right bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")
bottomLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical"
bottomRightInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")
bottomRightInvertedDirection: barIsVertical ? "horizontal" : "vertical"
// No border on the bar
borderWidth: 0
readonly property int bottomRightCornerState: {
// Floating bar: always simple rounded corners
if (barFloating)
return 0
// Bottom bar: bottom corners against screen edge = no radius
if (barPosition === "bottom")
return -1
// Right bar: bottom-right against screen edge = no radius
if (barPosition === "right")
return -1
// Top/Left bar with outerCorners: inverted corner
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2
}
// No outerCorners = square
return -1
}
MouseArea {
anchors.fill: parent
+1 -1
View File
@@ -1,7 +1,7 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.AudioSpectrum
import qs.Widgets.AudioSpectrum
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -2,7 +2,7 @@ import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -5,7 +5,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Modules.Bar.Extras
Item {
+1 -1
View File
@@ -3,7 +3,7 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Modules.AudioSpectrum
import qs.Widgets.AudioSpectrum
import qs.Commons
import qs.Services
import qs.Widgets
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
+1 -1
View File
@@ -4,7 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
+1 -10
View File
@@ -2,7 +2,7 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
@@ -353,13 +353,4 @@ Rectangle {
}
}
}
// MouseArea {
// anchors.fill: parent
// acceptedButtons: Qt.RightButton
// onClicked: {
// var directPanel = PanelService.getPanel("directWidgetSettingsPanel")
// directPanel.openWidgetSettings(root.section, root.sectionWidgetIndex, root.widgetId, root.widgetSettings)
// }
// }
}
+21 -21
View File
@@ -346,28 +346,28 @@ Rectangle {
}
}
PanelWindow {
id: trayPanel
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Color.transparent
screen: screen
// PanelWindow {
// id: trayPanel
// anchors.top: true
// anchors.left: true
// anchors.right: true
// anchors.bottom: true
// visible: false
// color: Color.transparent
// screen: screen
function open() {
visible = true
}
// function open() {
// visible = true
// }
function close() {
visible = false
}
// function close() {
// visible = false
// }
// Clicking outside of the rectangle to close
MouseArea {
anchors.fill: parent
onClicked: trayPanel.close()
}
}
// // Clicking outside of the rectangle to close
// MouseArea {
// anchors.fill: parent
// onClicked: trayPanel.close()
// }
// }
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
+1 -2
View File
@@ -11,8 +11,7 @@ import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.AudioSpectrum
import qs.Modules.Bar.Calendar
import qs.Widgets.AudioSpectrum
Loader {
id: lockScreen
@@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Effects
import qs.Commons
/**
* 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
anchors.fill: parent
// The unified Shape container
Shape {
id: backgroundsShape
anchors.fill: parent
// Use curve renderer for smooth corners
preferredRendererType: Shape.CurveRenderer
// CRITICAL: Shape must not block mouse input!
// ShapePaths inside will render, but the Shape container itself should be transparent to input
enabled: false // Disable mouse input on the Shape itself
Component.onCompleted: {
Logger.d("AllBackgrounds", "AllBackgrounds initialized")
Logger.d("AllBackgrounds", " bar:", root.bar)
Logger.d("AllBackgrounds", " windowRoot:", root.windowRoot)
}
/**
* Bar
*/
BarBackground {
bar: root.bar
shapeContainer: backgroundsShape
}
/**
* Panels
*/
// Audio
PanelBackground {
panel: root.windowRoot.audioPanel
shapeContainer: backgroundsShape
}
// Battery
PanelBackground {
panel: root.windowRoot.batteryPanel
shapeContainer: backgroundsShape
}
// Bluetooth
PanelBackground {
panel: root.windowRoot.bluetoothPanel
shapeContainer: backgroundsShape
}
// Calendar
PanelBackground {
panel: root.windowRoot.calendarPanel
shapeContainer: backgroundsShape
}
// Control Center
PanelBackground {
panel: root.windowRoot.controlCenterPanel
shapeContainer: backgroundsShape
}
// Launcher
PanelBackground {
panel: root.windowRoot.launcherPanel
shapeContainer: backgroundsShape
}
// Notification History
PanelBackground {
panel: root.windowRoot.notificationHistoryPanel
shapeContainer: backgroundsShape
}
// Session Menu
PanelBackground {
panel: root.windowRoot.sessionMenuPanel
shapeContainer: backgroundsShape
}
// Settings
PanelBackground {
panel: root.windowRoot.settingsPanel
shapeContainer: backgroundsShape
}
// Setup Wizard
PanelBackground {
panel: root.windowRoot.setupWizardPanel
shapeContainer: backgroundsShape
}
// TrayDropdown
PanelBackground {
panel: root.windowRoot.trayDropdownPanel
shapeContainer: backgroundsShape
}
// TrayMenu
PanelBackground {
panel: root.windowRoot.trayMenuPanel
shapeContainer: backgroundsShape
}
// Wallpaper
PanelBackground {
panel: root.windowRoot.wallpaperPanel
shapeContainer: backgroundsShape
}
// WiFi
PanelBackground {
panel: root.windowRoot.wifiPanel
shapeContainer: backgroundsShape
}
// // Tray Dropdown
// PanelBackground {
// panel: root.windowRoot.trayDropdownPanel
// shapeContainer: backgroundsShape
// }
}
// Unified shadow system (one MultiEffect for all backgrounds)
MultiEffect {
source: backgroundsShape
anchors.fill: backgroundsShape
shadowEnabled: true
shadowBlur: Style.shadowBlur
shadowColor: Color.black
shadowHorizontalOffset: Settings.data.general.shadowOffsetX
shadowVerticalOffset: Settings.data.general.shadowOffsetY
}
}
@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
/**
* 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
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// 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)
// Flatten corners if bar is too small (handle null bar)
readonly property bool shouldFlatten: bar ? (bar.width < radius * 2 || bar.height < radius * 2) : false
readonly property real effectiveRadius: shouldFlatten ? (bar ? Math.min(bar.width / 2, bar.height / 2) : 0) : radius
// Helper functions (inlined from ShapeCornerHelper)
function getMultX(cornerState) {
return cornerState === 1 ? -1 : 1
}
function getMultY(cornerState) {
return cornerState === 2 ? -1 : 1
}
function getArcDirection(multX, multY) {
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise
}
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
if (cornerState === -1)
return 0
// All other states use effectiveRadius
return effectiveRadius
}
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
readonly property real tlMultX: bar ? getMultX(bar.topLeftCornerState) : 1
readonly property real tlMultY: bar ? getMultY(bar.topLeftCornerState) : 1
readonly property real tlRadius: bar ? getCornerRadius(bar.topLeftCornerState) : 0
readonly property real trMultX: bar ? getMultX(bar.topRightCornerState) : 1
readonly property real trMultY: bar ? getMultY(bar.topRightCornerState) : 1
readonly property real trRadius: bar ? getCornerRadius(bar.topRightCornerState) : 0
readonly property real brMultX: bar ? getMultX(bar.bottomRightCornerState) : 1
readonly property real brMultY: bar ? getMultY(bar.bottomRightCornerState) : 1
readonly property real brRadius: bar ? getCornerRadius(bar.bottomRightCornerState) : 0
readonly property real blMultX: bar ? getMultX(bar.bottomLeftCornerState) : 1
readonly property real blMultY: bar ? getMultY(bar.bottomLeftCornerState) : 1
readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
fillColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
// Starting position (top-left corner, after the arc)
// Use mapped coordinates relative to the Shape container
startX: barMappedPos.x + tlRadius * tlMultX
startY: barMappedPos.y
// Smooth color animation
Behavior on fillColor {
ColorAnimation {
duration: Style.animationFast
}
}
// ========== PATH DEFINITION ==========
// Draws a rectangle with potentially inverted corners
// All coordinates are relative to startX/startY
// Top edge (moving right)
PathLine {
relativeX: (bar ? bar.width : 0) - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX
relativeY: 0
}
// Top-right corner arc
PathArc {
relativeX: root.trRadius * root.trMultX
relativeY: root.trRadius * root.trMultY
radiusX: root.trRadius
radiusY: root.trRadius
direction: root.getArcDirection(root.trMultX, root.trMultY)
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: (bar ? bar.height : 0) - root.trRadius * root.trMultY - root.brRadius * root.brMultY
}
// Bottom-right corner arc
PathArc {
relativeX: -root.brRadius * root.brMultX
relativeY: root.brRadius * root.brMultY
radiusX: root.brRadius
radiusY: root.brRadius
direction: root.getArcDirection(root.brMultX, root.brMultY)
}
// Bottom edge (moving left)
PathLine {
relativeX: -((bar ? bar.width : 0) - root.brRadius * root.brMultX - root.blRadius * root.blMultX)
relativeY: 0
}
// Bottom-left corner arc
PathArc {
relativeX: -root.blRadius * root.blMultX
relativeY: -root.blRadius * root.blMultY
radiusX: root.blRadius
radiusY: root.blRadius
direction: root.getArcDirection(root.blMultX, root.blMultY)
}
// Left edge (moving up) - closes the path back to start
PathLine {
relativeX: 0
relativeY: -((bar ? bar.height : 0) - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY)
}
// Top-left corner arc (back to start)
PathArc {
relativeX: root.tlRadius * root.tlMultX
relativeY: -root.tlRadius * root.tlMultY
radiusX: root.tlRadius
radiusY: root.tlRadius
direction: root.getArcDirection(root.tlMultX, root.tlMultY)
}
}
@@ -0,0 +1,174 @@
import QtQuick
import QtQuick.Shapes
import qs.Commons
/**
* PanelBackground - ShapePath component for rendering a single 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 panel component (e.g., windowRoot.controlCenterPanel)
required property var panel
// Required reference to AllBackgrounds shapeContainer
required property var shapeContainer
// Corner radius (from Style)
readonly property real radius: Style.radiusL
// Get the actual panelBackground Item from SmartPanel
// Only access panelRegion if panel exists and is visible
readonly property var panelBg: (panel && panel.visible) ? panel.panelRegion : null
// 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
// Flatten corners if panel is too small
readonly property bool shouldFlatten: panelBg ? (panelWidth < radius * 2 || panelHeight < radius * 2) : false
readonly property real effectiveRadius: shouldFlatten ? Math.min(panelWidth / 2, panelHeight / 2) : radius
// Helper functions (inlined from ShapeCornerHelper)
function getMultX(cornerState) {
return cornerState === 1 ? -1 : 1
}
function getMultY(cornerState) {
return cornerState === 2 ? -1 : 1
}
function getArcDirection(multX, multY) {
return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise
}
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
if (cornerState === -1)
return 0
// All other states use effectiveRadius
return effectiveRadius
}
// Per-corner multipliers and radii based on panelBg's corner states
readonly property real tlMultX: panelBg ? getMultX(panelBg.topLeftCornerState) : 1
readonly property real tlMultY: panelBg ? getMultY(panelBg.topLeftCornerState) : 1
readonly property real tlRadius: panelBg ? getCornerRadius(panelBg.topLeftCornerState) : 0
readonly property real trMultX: panelBg ? getMultX(panelBg.topRightCornerState) : 1
readonly property real trMultY: panelBg ? getMultY(panelBg.topRightCornerState) : 1
readonly property real trRadius: panelBg ? getCornerRadius(panelBg.topRightCornerState) : 0
readonly property real brMultX: panelBg ? getMultX(panelBg.bottomRightCornerState) : 1
readonly property real brMultY: panelBg ? getMultY(panelBg.bottomRightCornerState) : 1
readonly property real brRadius: panelBg ? getCornerRadius(panelBg.bottomRightCornerState) : 0
readonly property real blMultX: panelBg ? getMultX(panelBg.bottomLeftCornerState) : 1
readonly property real blMultY: panelBg ? getMultY(panelBg.bottomLeftCornerState) : 1
readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0
// DEBUG: Log panel state changes
onPanelChanged: {
Logger.d("PanelBackground", "=== panel changed:", panel)
Logger.d("PanelBackground", " panel.visible:", panel ? panel.visible : "null")
Logger.d("PanelBackground", " panel.panelRegion:", panel ? panel.panelRegion : "null")
}
onPanelBgChanged: {
Logger.d("PanelBackground", "=== panelBg changed:", panelBg)
if (panelBg) {
Logger.d("PanelBackground", " Geometry:", panelX, panelY, panelWidth, "x", panelHeight)
Logger.d("PanelBackground", " startX:", startX, "startY:", startY)
Logger.d("PanelBackground", " fillColor:", fillColor)
}
}
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
// Starting position (top-left corner, after the arc)
startX: panelX + tlRadius * tlMultX
startY: panelY
fillColor: Color.mSurface
// Smooth color animation
Behavior on fillColor {
ColorAnimation {
duration: Style.animationFast
}
}
// ========== PATH DEFINITION ==========
// Draws a rectangle with potentially inverted corners
// All coordinates are relative to startX/startY
// Top edge (moving right)
PathLine {
relativeX: root.panelWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX
relativeY: 0
}
// Top-right corner arc
PathArc {
relativeX: root.trRadius * root.trMultX
relativeY: root.trRadius * root.trMultY
radiusX: root.trRadius
radiusY: root.trRadius
direction: root.getArcDirection(root.trMultX, root.trMultY)
}
// Right edge (moving down)
PathLine {
relativeX: 0
relativeY: root.panelHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY
}
// Bottom-right corner arc
PathArc {
relativeX: -root.brRadius * root.brMultX
relativeY: root.brRadius * root.brMultY
radiusX: root.brRadius
radiusY: root.brRadius
direction: root.getArcDirection(root.brMultX, root.brMultY)
}
// Bottom edge (moving left)
PathLine {
relativeX: -(root.panelWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX)
relativeY: 0
}
// Bottom-left corner arc
PathArc {
relativeX: -root.blRadius * root.blMultX
relativeY: -root.blRadius * root.blMultY
radiusX: root.blRadius
radiusY: root.blRadius
direction: root.getArcDirection(root.blMultX, root.blMultY)
}
// Left edge (moving up) - closes the path back to start
PathLine {
relativeX: 0
relativeY: -(root.panelHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY)
}
// Top-left corner arc (back to start)
PathArc {
relativeX: root.tlRadius * root.tlMultX
relativeY: -root.tlRadius * root.tlMultY
radiusX: root.tlRadius
radiusY: root.tlRadius
direction: root.getArcDirection(root.tlMultX, root.tlMultY)
}
}
+74
View File
@@ -0,0 +1,74 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
/**
* 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
property bool exclusive: Settings.data.bar.exclusive !== undefined ? Settings.data.bar.exclusive : false
readonly property string barPosition: Settings.data.bar.position || "top"
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool barFloating: Settings.data.bar.floating || false
readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
// Invisible - just reserves space
color: "transparent"
mask: Region {}
// Wayland layer shell configuration
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
// Anchor based on bar position
anchors {
top: barPosition === "top"
bottom: barPosition === "bottom"
left: barPosition === "left" || barPosition === "top" || barPosition === "bottom"
right: barPosition === "right" || barPosition === "top" || barPosition === "bottom"
}
// Size based on bar orientation
// When floating, only reserve space for the bar + margin on the anchored edge
implicitWidth: {
if (barIsVertical) {
// Vertical bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For left bar, reserve left margin; for right bar, reserve right margin
return Style.barHeight + barMarginH
}
return Style.barHeight
}
return 0 // Auto-width when left/right anchors are true
}
implicitHeight: {
if (!barIsVertical) {
// Horizontal bar: reserve bar height + margin on the anchored edge only
if (barFloating) {
// For top bar, reserve top margin; for bottom bar, reserve bottom margin
return Style.barHeight + barMarginV
}
return Style.barHeight
}
return 0 // Auto-height when top/bottom anchors are true
}
Component.onCompleted: {
Logger.d("BarExclusionZone", "Created for screen:", screen?.name)
Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating)
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right)
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight)
}
}
+536
View File
@@ -0,0 +1,536 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import "Backgrounds" as Backgrounds
// All panels
import qs.Modules.Bar
import qs.Modules.Panels.Audio
import qs.Modules.Panels.Battery
import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Calendar
import qs.Modules.Panels.ControlCenter
import qs.Modules.Panels.Launcher
import qs.Modules.Panels.NotificationHistory
import qs.Modules.Panels.SessionMenu
import qs.Modules.Panels.Settings
import qs.Modules.Panels.SetupWizard
import qs.Modules.Panels.Tray
import qs.Modules.Panels.Wallpaper
import qs.Modules.Panels.WiFi
/**
* MainScreen - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
// Expose panels as readonly property aliases
readonly property alias audioPanel: audioPanel
readonly property alias batteryPanel: batteryPanel
readonly property alias bluetoothPanel: bluetoothPanel
readonly property alias calendarPanel: calendarPanel
readonly property alias controlCenterPanel: controlCenterPanel
readonly property alias launcherPanel: launcherPanel
readonly property alias notificationHistoryPanel: notificationHistoryPanel
readonly property alias sessionMenuPanel: sessionMenuPanel
readonly property alias settingsPanel: settingsPanel
readonly property alias setupWizardPanel: setupWizardPanel
readonly property alias trayDropdownPanel: trayDropdownPanel
readonly property alias trayMenuPanel: trayMenuPanel
readonly property alias wallpaperPanel: wallpaperPanel
readonly property alias wifiPanel: wifiPanel
Component.onCompleted: {
Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y)
}
// Debug: Log mask region changes
onMaskChanged: {
Logger.d("MainScreen", "Mask changed!")
Logger.d("MainScreen", " Bar region:", barLoader.item?.barRegion)
Logger.d("MainScreen", " Panel count:", panelsRepeater.count)
for (var i = 0; i < panelsRepeater.count; i++) {
var panelItem = panelsRepeater.itemAt(i)?.item
Logger.d("MainScreen", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion)
}
}
// Wayland
// Always use Exclusive keyboard focus when a panel is open
// This ensures all keyboard shortcuts work reliably (Escape, etc.)
// The centralized shortcuts in this window handle delegation to panels
WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.layer: Settings.data.ui.panelsOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that
anchors {
top: true
bottom: true
left: true
right: true
}
// Desktop dimming when panels are open
property bool dimDesktop: Settings.data.general.dimDesktop
property bool isPanelOpen: PanelService.openedPanel !== null
color: {
if (dimDesktop && isPanelOpen) {
return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy)
}
return Color.transparent
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
// Fully reactive mask system, make everything click-through except bar and open panels
mask: Region {
id: clickableMask
// Cover entire window (everything is masked/click-through)
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Xor
// Direct binding to Variants.instances plus additional regions
regions: panelRegions.instances.concat([barMaskRegion, backgroundMaskRegion])
// Bar region - subtract bar area from mask
Region {
id: barMaskRegion
property var barRegion: barLoader.item?.barRegion
x: barRegion?.x ?? 0
y: barRegion?.y ?? 0
width: barRegion?.width ?? 0
height: barRegion?.height ?? 0
intersection: Intersection.Subtract
}
// Background region for click-to-close - reactive sizing
Region {
id: backgroundMaskRegion
x: 0
y: 0
width: root.isPanelOpen ? root.width : 0
height: root.isPanelOpen ? root.height : 0
intersection: Intersection.Subtract
}
}
// Variants for panel regions
Variants {
id: panelRegions
// Model is the list of open panels (filters out closed ones)
model: [audioPanel, controlCenterPanel, calendarPanel].filter(function (p) {
return p && p.isPanelOpen
})
Region {
required property var modelData
property var panelBg: modelData.panelRegion
// Direct bindings
x: panelBg?.x ?? 0
y: panelBg?.y ?? 0
width: panelBg?.width ?? 0
height: panelBg?.height ?? 0
intersection: Intersection.Subtract
}
}
// 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: barLoader.item?.barItem || null
windowRoot: root
z: 0 // Behind all content
}
// Background MouseArea for closing panels when clicking outside
// Active whenever a panel is open - the mask ensures it only receives clicks when panel is open
MouseArea {
anchors.fill: parent
enabled: root.isPanelOpen
onClicked: {
if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
z: 0 // Behind panels and bar
}
// ---------------------------------------
// All panels always exist
// ---------------------------------------
AudioPanel {
id: audioPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "audioPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(audioPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
BatteryPanel {
id: batteryPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "batteryPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(batteryPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
BluetoothPanel {
id: bluetoothPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "bluetoothPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(bluetoothPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
ControlCenterPanel {
id: controlCenterPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "controlCenterPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(controlCenterPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
CalendarPanel {
id: calendarPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "calendarPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(calendarPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
Launcher {
id: launcherPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "launcherPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(launcherPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
NotificationHistoryPanel {
id: notificationHistoryPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "notificationHistoryPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(notificationHistoryPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
SessionMenu {
id: sessionMenuPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "sessionMenuPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(sessionMenuPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
SettingsPanel {
id: settingsPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "settingsPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(settingsPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
SetupWizard {
id: setupWizardPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "setupWizardPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(setupWizardPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
TrayDropdownPanel {
id: trayDropdownPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "trayDropdownPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(trayDropdownPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
TrayMenuPanel {
id: trayMenuPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "trayMenuPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(trayDropdownPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
WallpaperPanel {
id: wallpaperPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "wallpaperPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(wallpaperPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
WiFiPanel {
id: wifiPanel
screen: root.screen
z: 50
Component.onCompleted: {
objectName = "wifiPanel-" + (screen?.name || "unknown")
PanelService.registerPanel(wifiPanel)
Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name)
}
}
// Bar (always on top - rendered last in tree, so naturally on top)
Loader {
id: barLoader
asynchronous: false
sourceComponent: Bar {}
// Keep bar loaded but hide it when BarService.isVisible is false
// This allows panels to remain accessible via IPC
visible: BarService.isVisible
// Fill parent to provide dimensions for Bar to reference
anchors.fill: parent
property ShellScreen screen: root.screen
onLoaded: {
Logger.d("MainScreen", "Bar loaded:", item !== null)
if (item) {
Logger.d("MainScreen", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height)
// Bind screen to bar component (use binding for reactivity)
item.screen = Qt.binding(function () {
return barLoader.screen
})
}
}
}
}
// Centralized keyboard shortcuts - delegate to opened panel
// This ensures shortcuts work regardless of panel focus state
Shortcut {
sequence: "Escape"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) {
PanelService.openedPanel.onEscapePressed()
} else if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
}
Shortcut {
sequence: "Tab"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) {
PanelService.openedPanel.onTabPressed()
}
}
}
Shortcut {
sequence: "Shift+Tab"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) {
PanelService.openedPanel.onShiftTabPressed()
}
}
}
Shortcut {
sequence: "Up"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) {
PanelService.openedPanel.onUpPressed()
}
}
}
Shortcut {
sequence: "Down"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) {
PanelService.openedPanel.onDownPressed()
}
}
}
Shortcut {
sequence: "Return"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) {
PanelService.openedPanel.onReturnPressed()
}
}
}
Shortcut {
sequence: "Enter"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) {
PanelService.openedPanel.onReturnPressed()
}
}
}
Shortcut {
sequence: "Home"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) {
PanelService.openedPanel.onHomePressed()
}
}
}
Shortcut {
sequence: "End"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) {
PanelService.openedPanel.onEndPressed()
}
}
}
Shortcut {
sequence: "PgUp"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) {
PanelService.openedPanel.onPageUpPressed()
}
}
}
Shortcut {
sequence: "PgDown"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) {
PanelService.openedPanel.onPageDownPressed()
}
}
}
Shortcut {
sequence: "Ctrl+J"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) {
PanelService.openedPanel.onCtrlJPressed()
}
}
}
Shortcut {
sequence: "Ctrl+K"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) {
PanelService.openedPanel.onCtrlKPressed()
}
}
}
Shortcut {
sequence: "Left"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) {
PanelService.openedPanel.onLeftPressed()
}
}
}
Shortcut {
sequence: "Right"
enabled: root.isPanelOpen
onActivated: {
if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) {
PanelService.openedPanel.onRightPressed()
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
pragma Singleton
import QtQuick
import QtQuick.Shapes
/**
* 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.
*/
QtObject {
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)
}
/**
* Calculate the starting X position for a shape, accounting for top-left corner
* This is used to set ShapePath's startX position
*/
function getStartX(x, radius, topLeftState) {
return x + radius * getMultX(topLeftState)
}
/**
* Calculate the starting Y position for a shape
* In most cases this is just the y position, but can be adjusted for special cases
*/
function getStartY(y) {
return y
}
/**
* 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
}
}
+634
View File
@@ -0,0 +1,634 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services
/**
* SmartPanel for use within MainScreen
*/
Item {
id: root
// Screen property provided by MainScreen
property ShellScreen screen: null
// Panel content: Text, icons, etc...
property Component panelContent: null
// Panel size properties
property real preferredWidth: 700
property real preferredHeight: 900
property real preferredWidthRatio
property real preferredHeightRatio
property color panelBackgroundColor: Color.mSurface
property color panelBorderColor: Color.mOutline
property var buttonItem: null
// Anchoring properties
property bool panelAnchorHorizontalCenter: false
property bool panelAnchorVerticalCenter: false
property bool panelAnchorTop: false
property bool panelAnchorBottom: false
property bool panelAnchorLeft: false
property bool panelAnchorRight: false
// Button position properties
property bool useButtonPosition: false
property point buttonPosition: Qt.point(0, 0)
property int buttonWidth: 0
property int buttonHeight: 0
// Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap
property real edgeSnapDistance: 50
// Track whether panel is open
property bool isPanelOpen: false
// Track actual visibility (delayed until content is loaded and sized)
property bool isPanelVisible: false
// Keyboard event handlers - override these in specific panels to handle shortcuts
// These are called from MainScreen's centralized shortcuts
function onEscapePressed() {
close()
}
function onTabPressed() {}
function onShiftTabPressed() {}
function onUpPressed() {}
function onDownPressed() {}
function onLeftPressed() {}
function onRightPressed() {}
function onReturnPressed() {}
function onHomePressed() {}
function onEndPressed() {}
function onPageUpPressed() {}
function onPageDownPressed() {}
function onCtrlJPressed() {}
function onCtrlKPressed() {}
// Expose panel region for click-through mask
readonly property var panelRegion: panelContent.maskRegion
readonly property string barPosition: Settings.data.bar.position
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool barFloating: Settings.data.bar.floating
readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
// Helper to detect if any anchor is explicitly set
readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight
readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom
// Effective anchor properties (depend on couldAttach)
// These are true when:
// 1. Explicitly anchored, OR
// 2. Using button position and bar is on that edge, OR
// 3. Attached to bar with no explicit anchors (default centering behavior)
readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") || (panelContent.couldAttach && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical)
readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (panelContent.couldAttach && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical)
readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (panelContent.couldAttach && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical)
readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (panelContent.couldAttach && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical)
signal opened
signal closed
// Panel visibility and sizing
visible: isPanelVisible
width: parent ? parent.width : 0
height: parent ? parent.height : 0
// Panel control functions
function toggle(buttonItem, buttonName) {
if (!isPanelOpen) {
open(buttonItem, buttonName)
} else {
close()
}
}
function open(buttonItem, buttonName) {
if (!buttonItem && buttonName) {
buttonItem = BarService.lookupWidget(buttonName, screen.name)
}
if (buttonItem) {
root.buttonItem = buttonItem
// Map button position to screen coordinates
var buttonPos = buttonItem.mapToItem(null, 0, 0)
root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y)
root.buttonWidth = buttonItem.width
root.buttonHeight = buttonItem.height
root.useButtonPosition = true
} else {
// No button provided: reset button position mode
root.buttonItem = null
root.useButtonPosition = false
}
// Set isPanelOpen to trigger content loading, but don't show yet
isPanelOpen = true
// Notify PanelService
PanelService.willOpenPanel(root)
// Position and visibility will be set by Loader.onLoaded
// This ensures no flicker from default size to content size
}
function close() {
// Set visibility to false - state transition will animate
isPanelVisible = false
isPanelOpen = false
PanelService.closedPanel(root)
closed()
Logger.d("SmartPanel", "Closing panel", objectName)
}
function setPosition() {
// Calculate panel dimensions first (needed for positioning)
var w
// Priority 1: Content-driven size (dynamic)
if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) {
w = contentLoader.item.contentPreferredWidth
} // Priority 2: Ratio-based size
else if (root.preferredWidthRatio !== undefined) {
w = Math.round(Math.max((root.width || 1920) * root.preferredWidthRatio, root.preferredWidth))
} // Priority 3: Static preferred width
else {
w = root.preferredWidth
}
var panelWidth = Math.min(w, (root.width || 1920) - Style.marginL * 2)
var h
// Priority 1: Content-driven size (dynamic)
if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) {
h = contentLoader.item.contentPreferredHeight
} // Priority 2: Ratio-based size
else if (root.preferredHeightRatio !== undefined) {
h = Math.round(Math.max((root.height || 1080) * root.preferredHeightRatio, root.preferredHeight))
} // Priority 3: Static preferred height
else {
h = root.preferredHeight
}
var panelHeight = Math.min(h, (root.height || 1080) - Style.barHeight - Style.marginL * 2)
// Update panelBackground size
panelBackground.width = panelWidth
panelBackground.height = panelHeight
// Calculate position
var calculatedX
var calculatedY
// ===== X POSITIONING =====
if (root.useButtonPosition && root.width > 0 && panelWidth > 0) {
if (root.barIsVertical) {
// For vertical bars
if (panelContent.couldAttach) {
// Attached panels: align with bar edge (left or right side)
if (root.barPosition === "left") {
var leftBarEdge = root.barMarginH + Style.barHeight
calculatedX = leftBarEdge
} else {
// right
var rightBarEdge = root.width - root.barMarginH - Style.barHeight
calculatedX = rightBarEdge - panelWidth
}
} else {
// Detached panels: center on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2
var minX = Style.marginL
var maxX = root.width - panelWidth - Style.marginL
// Account for vertical bar taking up space
if (root.barPosition === "left") {
minX = root.barMarginH + Style.barHeight + Style.marginL
} else if (root.barPosition === "right") {
maxX = root.width - root.barMarginH - Style.barHeight - panelWidth - Style.marginL
}
panelX = Math.max(minX, Math.min(panelX, maxX))
calculatedX = panelX
}
} else {
// For horizontal bars, center panel on button X position
var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2
if (panelContent.couldAttach) {
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = root.width - root.barMarginH - cornerInset
panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - panelWidth))
} else {
panelX = Math.max(Style.marginL, Math.min(panelX, root.width - panelWidth - Style.marginL))
}
calculatedX = panelX
}
} else {
// Standard anchor positioning
if (root.panelAnchorHorizontalCenter) {
if (root.barIsVertical) {
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = root.width - availableStart
calculatedX = availableStart + (availableWidth - panelWidth) / 2
} else if (root.barPosition === "right") {
var availableWidth = root.width - root.barMarginH - Style.barHeight
calculatedX = (availableWidth - panelWidth) / 2
} else {
calculatedX = (root.width - panelWidth) / 2
}
} else {
calculatedX = (root.width - panelWidth) / 2
}
} else if (root.effectivePanelAnchorRight) {
if (panelContent.couldAttach && root.barIsVertical && root.barPosition === "right") {
var rightBarEdge = root.width - root.barMarginH - Style.barHeight
calculatedX = rightBarEdge - panelWidth
} else if (panelContent.couldAttach) {
calculatedX = root.width - panelWidth
} else {
calculatedX = root.width - panelWidth - Style.marginL
}
} else if (root.effectivePanelAnchorLeft) {
if (panelContent.couldAttach && root.barIsVertical && root.barPosition === "left") {
var leftBarEdge = root.barMarginH + Style.barHeight
calculatedX = leftBarEdge
} else if (panelContent.couldAttach) {
calculatedX = 0
} else {
calculatedX = Style.marginL
}
} else {
// No explicit anchor: default to centering on bar
if (root.barIsVertical) {
if (root.barPosition === "left") {
var availableStart = root.barMarginH + Style.barHeight
var availableWidth = root.width - availableStart - Style.marginL
calculatedX = availableStart + (availableWidth - panelWidth) / 2
} else {
var availableWidth = root.width - root.barMarginH - Style.barHeight - Style.marginL
calculatedX = Style.marginL + (availableWidth - panelWidth) / 2
}
} else {
if (panelContent.couldAttach) {
var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0)
var barLeftEdge = root.barMarginH + cornerInset
var barRightEdge = root.width - root.barMarginH - cornerInset
var centeredX = (root.width - panelWidth) / 2
calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - panelWidth))
} else {
calculatedX = (root.width - panelWidth) / 2
}
}
}
}
// Edge snapping for X
if (panelContent.couldAttach && !root.barFloating && root.width > 0 && panelWidth > 0) {
var leftEdgePos = root.barMarginH
if (root.barPosition === "left") {
leftEdgePos = root.barMarginH + Style.barHeight
}
var rightEdgePos = root.width - root.barMarginH - panelWidth
if (root.barPosition === "right") {
rightEdgePos = root.width - root.barMarginH - Style.barHeight - panelWidth
}
if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) {
calculatedX = leftEdgePos
} else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) {
calculatedX = rightEdgePos
}
}
// ===== Y POSITIONING =====
if (root.useButtonPosition && root.height > 0 && panelHeight > 0) {
if (root.barPosition === "top") {
var topBarEdge = root.barMarginV + Style.barHeight
if (panelContent.couldAttach) {
calculatedY = topBarEdge
} else {
calculatedY = topBarEdge + Style.marginM
}
} else if (root.barPosition === "bottom") {
var bottomBarEdge = root.height - root.barMarginV - Style.barHeight
if (panelContent.couldAttach) {
calculatedY = bottomBarEdge - panelHeight
} else {
calculatedY = bottomBarEdge - panelHeight - Style.marginM
}
} else if (root.barIsVertical) {
var panelY = root.buttonPosition.y + root.buttonHeight / 2 - panelHeight / 2
var extraPadding = (panelContent.couldAttach && root.barFloating) ? Style.radiusL : 0
if (panelContent.couldAttach) {
var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0)
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = root.height - root.barMarginV - cornerInset
panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - panelHeight))
} else {
panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, root.height - panelHeight - Style.marginL - extraPadding))
}
calculatedY = panelY
}
} else {
// Standard anchor positioning
var barOffset = 0
if (!panelContent.couldAttach) {
if (root.barPosition === "top") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
} else if (root.barPosition === "bottom") {
barOffset = root.barMarginV + Style.barHeight + Style.marginM
}
} else {
if (root.effectivePanelAnchorTop && root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
} else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
} else if (!root.hasExplicitVerticalAnchor) {
if (root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
} else if (root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
}
}
}
if (calculatedY === undefined) {
if (root.panelAnchorVerticalCenter) {
if (!root.barIsVertical) {
if (root.barPosition === "top") {
var availableStart = root.barMarginV + Style.barHeight
var availableHeight = root.height - availableStart
calculatedY = availableStart + (availableHeight - panelHeight) / 2
} else if (root.barPosition === "bottom") {
var availableHeight = root.height - root.barMarginV - Style.barHeight
calculatedY = (availableHeight - panelHeight) / 2
} else {
calculatedY = (root.height - panelHeight) / 2
}
} else {
calculatedY = (root.height - panelHeight) / 2
}
} else if (root.effectivePanelAnchorTop) {
if (panelContent.couldAttach) {
calculatedY = 0
} else {
var topBarOffset = (root.barPosition === "top") ? barOffset : 0
calculatedY = topBarOffset + Style.marginL
}
} else if (root.effectivePanelAnchorBottom) {
if (panelContent.couldAttach) {
calculatedY = root.height - panelHeight
} else {
var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0
calculatedY = root.height - panelHeight - bottomBarOffset - Style.marginL
}
} else {
if (root.barIsVertical) {
if (panelContent.couldAttach) {
var cornerInset = root.barFloating ? Style.radiusL * 2 : 0
var barTopEdge = root.barMarginV + cornerInset
var barBottomEdge = root.height - root.barMarginV - cornerInset
var centeredY = (root.height - panelHeight) / 2
calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - panelHeight))
} else {
calculatedY = (root.height - panelHeight) / 2
}
} else {
if (panelContent.couldAttach && !root.barIsVertical) {
if (root.barPosition === "top") {
calculatedY = root.barMarginV + Style.barHeight
} else if (root.barPosition === "bottom") {
calculatedY = root.height - root.barMarginV - Style.barHeight - panelHeight
}
} else {
if (root.barPosition === "top") {
calculatedY = barOffset + Style.marginL
} else if (root.barPosition === "bottom") {
calculatedY = Style.marginL
} else {
calculatedY = Style.marginL
}
}
}
}
}
}
// Edge snapping for Y
if (panelContent.couldAttach && !root.barFloating && root.height > 0 && panelHeight > 0) {
var topEdgePos = root.barMarginV
if (root.barPosition === "top") {
topEdgePos = root.barMarginV + Style.barHeight
}
var bottomEdgePos = root.height - root.barMarginV - panelHeight
if (root.barPosition === "bottom") {
bottomEdgePos = root.height - root.barMarginV - Style.barHeight - panelHeight
}
if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) {
calculatedY = topEdgePos
} else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) {
calculatedY = bottomEdgePos
}
}
// Apply calculated positions
panelBackground.x = calculatedX
panelBackground.y = calculatedY
Logger.d("SmartPanel", "Position calculated:", calculatedX, calculatedY)
Logger.d("SmartPanel", " Panel size:", panelWidth, "x", panelHeight)
}
// Watch for changes in content-driven sizes and update position
Connections {
target: contentLoader.item
ignoreUnknownSignals: true
function onContentPreferredWidthChanged() {
if (root.isPanelOpen && root.isPanelVisible) {
root.setPosition()
}
}
function onContentPreferredHeightChanged() {
if (root.isPanelOpen && root.isPanelVisible) {
root.setPosition()
}
}
}
// Simple opacity-based animation
opacity: isPanelVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
// ------------------------------------------------
// Panel Content
Item {
id: panelContent
anchors.fill: parent
// Screen-dependent attachment properties
readonly property bool couldAttach: Settings.data.ui.panelsAttachedToBar
readonly property bool couldAttachToBar: {
if (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity < 1.0) {
return false
}
// A panel can only be attached to a bar if there is a bar on that screen
var monitors = Settings.data.bar.monitors || []
var result = monitors.length === 0 || monitors.includes(root.screen?.name || "")
return result
}
// Edge detection - detect if panel is touching screen edges
readonly property bool touchingLeftEdge: couldAttach && panelBackground.x <= 1
readonly property bool touchingRightEdge: couldAttach && (panelBackground.x + panelBackground.width) >= (root.width - 1)
readonly property bool touchingTopEdge: couldAttach && panelBackground.y <= 1
readonly property bool touchingBottomEdge: couldAttach && (panelBackground.y + panelBackground.height) >= (root.height - 1)
// Bar edge detection - detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints)
readonly property bool touchingTopBar: couldAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.y - (root.barMarginV + Style.barHeight)) <= 1
readonly property bool touchingBottomBar: couldAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((panelBackground.y + panelBackground.height) - (root.height - root.barMarginV - Style.barHeight)) <= 1
readonly property bool touchingLeftBar: couldAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(panelBackground.x - (root.barMarginH + Style.barHeight)) <= 1
readonly property bool touchingRightBar: couldAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((panelBackground.x + panelBackground.width) - (root.width - root.barMarginH - Style.barHeight)) <= 1
// Expose panelBackground for mask region
property alias maskRegion: panelBackground
// The actual panel background - provides geometry for PanelBackground rendering
Item {
id: panelBackground
x: root.x
y: root.y
width: root.preferredWidth
height: root.preferredHeight
// Corner states for PanelBackground to read
// 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)
// Smart corner state calculation based on bar attachment and edge touching
property int topLeftCornerState: {
// Bar attachment: only attach to bar if bar opacity >= 1.0 (no color clash)
var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft))
// Also detect when panel touches bar edge (e.g., centered panel that's too tall)
var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingLeftBar
// Screen edge contact: can attach to screen edges even if bar opacity < 1.0
// For horizontal bars: invert when touching left/right edges
// For vertical bars: invert when touching top/bottom edges
var edgeInverted = panelContent.couldAttach && ((panelContent.touchingLeftEdge && !root.barIsVertical) || (panelContent.touchingTopEdge && root.barIsVertical))
// Also invert when touching screen edge opposite to bar (e.g., bottom edge when bar is at top)
var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
// Determine direction: horizontal bars → horizontal curves, vertical bars → vertical curves
// Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves
if (panelContent.touchingLeftEdge && !root.barIsVertical)
return 2 // Vertical inversion
if (panelContent.touchingTopEdge && root.barIsVertical)
return 1 // Horizontal inversion
return root.barIsVertical ? 2 : 1
}
return 0 // Normal corner
}
property int topRightCornerState: {
var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight))
var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingRightBar
var edgeInverted = panelContent.couldAttach && ((panelContent.touchingRightEdge && !root.barIsVertical) || (panelContent.touchingTopEdge && root.barIsVertical))
var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingTopEdge && !root.barIsVertical && root.barPosition !== "top")
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (panelContent.touchingRightEdge && !root.barIsVertical)
return 2
if (panelContent.touchingTopEdge && root.barIsVertical)
return 1
return root.barIsVertical ? 2 : 1
}
return 0
}
property int bottomLeftCornerState: {
var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft))
var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingLeftBar
var edgeInverted = panelContent.couldAttach && ((panelContent.touchingLeftEdge && !root.barIsVertical) || (panelContent.touchingBottomEdge && root.barIsVertical))
var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (panelContent.touchingLeftEdge && !root.barIsVertical)
return 2
if (panelContent.touchingBottomEdge && root.barIsVertical)
return 1
return root.barIsVertical ? 2 : 1
}
return 0
}
property int bottomRightCornerState: {
var barInverted = panelContent.couldAttachToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight))
var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingRightBar
var edgeInverted = panelContent.couldAttach && ((panelContent.touchingRightEdge && !root.barIsVertical) || (panelContent.touchingBottomEdge && root.barIsVertical))
var oppositeEdgeInverted = panelContent.couldAttach && (panelContent.touchingBottomEdge && !root.barIsVertical && root.barPosition !== "bottom")
if (barInverted || barTouchInverted || edgeInverted || oppositeEdgeInverted) {
if (panelContent.touchingRightEdge && !root.barIsVertical)
return 2
if (panelContent.touchingBottomEdge && root.barIsVertical)
return 1
return root.barIsVertical ? 2 : 1
}
return 0
}
// MouseArea to catch clicks on the panel and prevent them from reaching the background
// This prevents closing the panel when clicking inside it
MouseArea {
anchors.fill: parent
z: -1 // Behind content, but on the panel background
onClicked: {
// Accept and ignore - prevents propagation to background
}
}
}
// Panel top content: Text, icons, etc...
Loader {
id: contentLoader
active: isPanelOpen
x: panelBackground.x
y: panelBackground.y
width: panelBackground.width
height: panelBackground.height
sourceComponent: root.panelContent
// When content finishes loading, make visible then calculate position
onLoaded: {
// Make panel visible FIRST so corner state bindings have valid context
// This happens in the same frame, so no visual flicker
isPanelVisible = true
// Calculate position with content-driven sizes if available
// When position updates, corner state bindings re-evaluate with panel visible
setPosition()
// Emit opened signal
opened()
}
}
}
}
@@ -1,329 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import qs.Commons
import qs.Services
import qs.Widgets
// Notification History panel
NPanel {
id: root
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 480 * Style.uiScaleRatio
onOpened: function () {
NotificationService.updateLastSeenTs()
}
panelContent: Rectangle {
id: notificationRect
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header section
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: "bell"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("notifications.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("tooltips.clear-history")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
NotificationService.clearHistory()
// Close panel as there is nothing more to see.
root.close()
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Empty state when no notifications
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyList.count === 0
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NIcon {
icon: "bell-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("notifications.panel.no-notifications")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("notifications.panel.description")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Notification list
NListView {
id: notificationList
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.historyList
spacing: Style.marginM
clip: true
boundsBehavior: Flickable.StopAtBounds
visible: NotificationService.historyList.count > 0
// Track which notification is expanded
property string expandedId: ""
delegate: Rectangle {
property string notificationId: model.id
property bool isExpanded: notificationList.expandedId === notificationId
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * 2)
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
border.width: Style.borderS
Behavior on height {
enabled: !Settings.data.general.animationDisabled
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
// Smooth color transition on hover
Behavior on color {
enabled: !Settings.data.general.animationDisabled
ColorAnimation {
duration: Style.animationFast
}
}
// Click to expand/collapse
MouseArea {
anchors.fill: parent
// Don't capture clicks on the delete button
anchors.rightMargin: 48
enabled: (summaryText.truncated || bodyText.truncated)
onClicked: {
if (notificationList.expandedId === notificationId) {
notificationList.expandedId = ""
} else {
notificationList.expandedId = notificationId
}
}
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
RowLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
ColumnLayout {
NImageCircled {
Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio)
Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio)
Layout.alignment: Qt.AlignTop
Layout.topMargin: 20
imagePath: model.cachedImage || model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
Item {
Layout.fillHeight: true
}
}
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: Style.marginXS
Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6)
// Header row with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
// Urgency indicator
Rectangle {
Layout.preferredWidth: 6
Layout.preferredHeight: 6
Layout.alignment: Qt.AlignVCenter
radius: 3
visible: model.urgency !== 1
color: {
if (model.urgency === 2)
return Color.mError
else if (model.urgency === 0)
return Color.mOnSurfaceVariant
else
return Color.transparent
}
}
NText {
text: model.appName || "Unknown App"
pointSize: Style.fontSizeXS
color: Color.mSecondary
}
NText {
text: Time.formatRelativeTime(model.timestamp)
pointSize: Style.fontSizeXS
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
}
// Summary
NText {
id: summaryText
text: model.summary || I18n.tr("general.no-summary")
pointSize: Style.fontSizeM
font.weight: Font.Medium
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: isExpanded ? 999 : 2
elide: Text.ElideRight
}
// Body
NText {
id: bodyText
text: model.body || ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: isExpanded ? 999 : 3
elide: Text.ElideRight
visible: text.length > 0
}
// Spacer for expand indicator
Item {
Layout.fillWidth: true
Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0
}
// Expand indicator
RowLayout {
Layout.fillWidth: true
visible: !isExpanded && (summaryText.truncated || bodyText.truncated)
spacing: Style.marginXS
Item {
Layout.fillWidth: true
}
NText {
text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand"
pointSize: Style.fontSizeXS
color: Color.mPrimary
font.weight: Font.Medium
}
NIcon {
icon: "chevron-down"
pointSize: Style.fontSizeS
color: Color.mPrimary
}
}
}
// Delete button
NIconButton {
icon: "trash"
tooltipText: I18n.tr("tooltips.delete-notification")
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
// Remove from history using the service API
NotificationService.removeFromHistory(notificationId)
}
}
}
}
}
}
}
}
@@ -6,8 +6,9 @@ import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
property real localOutputVolume: AudioService.volume || 0
@@ -53,9 +54,7 @@ NPanel {
}
}
panelContent: Rectangle {
color: Color.transparent
panelContent: Item {
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2
@@ -6,8 +6,9 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
property var optionsModel: []
@@ -7,8 +7,9 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
preferredWidth: 420 * Style.uiScaleRatio
@@ -4,14 +4,14 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.ControlCenter.Cards
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
import qs.Modules.Panels.ControlCenter.Cards
NPanel {
SmartPanel {
id: root
property ShellScreen screen
readonly property var now: Time.now
preferredWidth: 500 * Style.uiScaleRatio
@@ -32,9 +32,12 @@ NPanel {
panelContent: Item {
anchors.fill: parent
// Dynamic sizing properties that NPanel will bind to
// Dynamic sizing properties that SmartPanel will bind to
property real contentPreferredWidth: (Settings.data.location.showWeekNumberInCalendar ? 400 : 380) * Style.uiScaleRatio
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2
property real calendarGridHeight: {
// Calculate number of weeks in the calendar grid
const numWeeks = grid.daysModel ? Math.ceil(grid.daysModel.length / 7) : 5
@@ -44,10 +47,6 @@ NPanel {
return numWeeks * rowHeight
}
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2
ColumnLayout {
id: content
anchors.fill: parent
@@ -3,7 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import qs.Modules.AudioSpectrum
import qs.Widgets.AudioSpectrum
import qs.Commons
import qs.Services
import qs.Widgets
@@ -4,11 +4,11 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Modules.Settings
import qs.Modules.ControlCenter
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Panels.ControlCenter.Cards
import qs.Modules.Panels.Settings
// Header card with avatar, user and quick actions
NBox {
@@ -5,8 +5,8 @@ import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Cards
import qs.Modules.Panels.ControlCenter
import qs.Modules.Panels.ControlCenter.Cards
RowLayout {
Layout.fillWidth: true
@@ -46,7 +46,7 @@ NBox {
Layout.alignment: Qt.AlignHCenter
}
NCircleStat {
value: SystemStatService.diskPercents["/"]
value: SystemStatService.diskPercents["/"] ?? 0
icon: "storage"
flat: true
contentScale: 0.8
@@ -2,14 +2,18 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Modules.ControlCenter.Cards
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
import qs.Modules.Panels.ControlCenter.Cards
NPanel {
SmartPanel {
id: root
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
preferredWidth: Math.round(460 * Style.uiScaleRatio)
preferredHeight: {
var height = 0
@@ -57,15 +61,14 @@ NPanel {
}
// When position is "close_to_bar_button" but there's no bar, fall back to center
readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen
panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center"))
panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center"
panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
// readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen
// panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center"))
// panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center"
// panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left")
// panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right")
// panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_")
// panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_")
readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio)
readonly property int shortcutsHeight: Math.round(52 * Style.uiScaleRatio)
readonly property int audioHeight: Math.round(60 * Style.uiScaleRatio)
@@ -1,7 +1,7 @@
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
@@ -6,8 +6,10 @@ import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
import "Plugins"
NPanel {
SmartPanel {
id: root
// Panel configuration
@@ -49,7 +51,7 @@ NPanel {
readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6)
readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2)
// Override keyboard handlers from NPanel for navigation
// Override keyboard handlers from SmartPanel for navigation
function onTabPressed() {
selectNextWrapped()
}
@@ -151,14 +153,14 @@ NPanel {
resultsReady = false
ignoreMouseHover = true
// Notify plugins
for (let plugin of plugins) {
if (plugin.onOpened)
plugin.onOpened()
}
updateResults()
// Notify plugins and update results
// Use Qt.callLater to ensure plugins are registered (Component.onCompleted runs first)
Qt.callLater(() => {
for (let plugin of plugins) {
if (plugin.onOpened)
plugin.onOpened()
}
updateResults()
resultsReady = true
})
}
@@ -175,33 +177,28 @@ NPanel {
}
}
// Load plugins
Component.onCompleted: {
// Load applications plugin
const appsPlugin = Qt.createComponent("Plugins/ApplicationsPlugin.qml").createObject(this)
if (appsPlugin) {
registerPlugin(appsPlugin)
// Plugin components - declared inline so imports work correctly
ApplicationsPlugin {
id: appsPlugin
Component.onCompleted: {
registerPlugin(this)
Logger.d("Launcher", "Registered: ApplicationsPlugin")
} else {
Logger.e("Launcher", "Failed to load ApplicationsPlugin")
}
}
// Load calculator plugin
const calcPlugin = Qt.createComponent("Plugins/CalculatorPlugin.qml").createObject(this)
if (calcPlugin) {
registerPlugin(calcPlugin)
CalculatorPlugin {
id: calcPlugin
Component.onCompleted: {
registerPlugin(this)
Logger.d("Launcher", "Registered: CalculatorPlugin")
} else {
Logger.e("Launcher", "Failed to load CalculatorPlugin")
}
}
// Load clipboard history plugin
const clipboardPlugin = Qt.createComponent("Plugins/ClipboardPlugin.qml").createObject(this)
if (clipboardPlugin) {
registerPlugin(clipboardPlugin)
ClipboardPlugin {
id: clipPlugin
Component.onCompleted: {
registerPlugin(this)
Logger.d("Launcher", "Registered: ClipboardPlugin")
} else {
Logger.e("Launcher", "Failed to load ClipboardPlugin")
}
}
@@ -3,7 +3,7 @@ import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import "../../../Helpers/FuzzySort.js" as Fuzzysort
import "../../../../Helpers/FuzzySort.js" as Fuzzysort
Item {
property var launcher: null
@@ -194,7 +194,7 @@ Item {
"icon": app.icon || "application-x-executable",
"isImage": false,
"onActivate": function () {
// Close the launcher/NPanel immediately without any animations.
// Close the launcher/SmartPanel immediately without any animations.
// Ensures we are not preventing the future focusing of the app
launcher.close()
@@ -1,7 +1,7 @@
import QtQuick
import qs.Services
import qs.Commons
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
import "../../../../Helpers/AdvancedMath.js" as AdvancedMath
Item {
property var launcher: null
@@ -0,0 +1,343 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
// Notification History panel
SmartPanel {
id: root
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 480 * Style.uiScaleRatio
onOpened: function () {
NotificationService.updateLastSeenTs()
}
panelContent: Rectangle {
id: notificationRect
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
// Header section
NBox {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight + (Style.marginM * 2)
RowLayout {
id: headerRow
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NIcon {
icon: "bell"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
}
NText {
text: I18n.tr("notifications.panel.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled")
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("tooltips.clear-history")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
NotificationService.clearHistory()
// Close panel as there is nothing more to see.
root.close()
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
}
// Empty state when no notifications
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyList.count === 0
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
NIcon {
icon: "bell-off"
pointSize: 64
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("notifications.panel.no-notifications")
pointSize: Style.fontSizeL
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("notifications.panel.description")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
// Notification list
// NListView {
// id: notificationList
// Layout.fillWidth: true
// Layout.fillHeight: true
// horizontalPolicy: ScrollBar.AlwaysOff
// verticalPolicy: ScrollBar.AsNeeded
// model: NotificationService.historyList
// spacing: Style.marginM
// clip: true
// boundsBehavior: Flickable.StopAtBounds
// visible: NotificationService.historyList.count > 0
// // Track which notification is expanded
// property string expandedId: ""
// delegate: Item {
// property string notificationId: model.id
// property bool isExpanded: notificationList.expandedId === notificationId
// width: notificationList.width
// height: notificationLayoutWrapper.height + (Style.marginM * 2)
// Behavior on height {
// enabled: !Settings.data.general.animationDisabled
// NumberAnimation {
// duration: Style.animationNormal
// easing.type: Easing.InOutQuad
// }
// }
// Rectangle {
// anchors.fill: parent
// radius: Style.radiusM
// color: Color.mSurfaceVariant
// border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
// border.width: Style.borderS
// // Smooth color transition on hover
// Behavior on color {
// enabled: !Settings.data.general.animationDisabled
// ColorAnimation {
// duration: Style.animationFast
// }
// }
// }
// // Click to expand/collapse
// MouseArea {
// anchors.fill: parent
// // Don't capture clicks on the delete button
// anchors.rightMargin: 48
// enabled: (summaryText.truncated || bodyText.truncated)
// onClicked: {
// if (notificationList.expandedId === notificationId) {
// notificationList.expandedId = ""
// } else {
// notificationList.expandedId = notificationId
// }
// }
// cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
// }
// Item {
// id: notificationLayoutWrapper
// anchors.left: parent.left
// anchors.right: parent.right
// anchors.top: parent.top
// anchors.margins: Style.marginM
// height: notificationLayout.implicitHeight
// RowLayout {
// id: notificationLayout
// width: parent.width
// spacing: Style.marginM
// // Icon column - use simple Item instead of ColumnLayout to avoid polish loop
// Item {
// Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio)
// Layout.alignment: Qt.AlignTop
// NImageCircled {
// anchors.top: parent.top
// anchors.topMargin: 20
// width: Math.round(40 * Style.uiScaleRatio)
// height: Math.round(40 * Style.uiScaleRatio)
// imagePath: model.cachedImage || model.originalImage || ""
// borderColor: Color.transparent
// borderWidth: 0
// fallbackIcon: "bell"
// fallbackIconSize: 24
// }
// }
// // Notification content column
// ColumnLayout {
// Layout.fillWidth: true
// Layout.alignment: Qt.AlignTop
// spacing: Style.marginXS
// Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6)
// // Header row with app name and timestamp
// RowLayout {
// Layout.fillWidth: true
// spacing: Style.marginS
// // Urgency indicator
// Rectangle {
// Layout.preferredWidth: 6
// Layout.preferredHeight: 6
// Layout.alignment: Qt.AlignVCenter
// radius: 3
// visible: model.urgency !== 1
// color: {
// if (model.urgency === 2)
// return Color.mError
// else if (model.urgency === 0)
// return Color.mOnSurfaceVariant
// else
// return Color.transparent
// }
// }
// NText {
// text: model.appName || "Unknown App"
// pointSize: Style.fontSizeXS
// color: Color.mSecondary
// }
// NText {
// text: Time.formatRelativeTime(model.timestamp)
// pointSize: Style.fontSizeXS
// color: Color.mSecondary
// }
// Item {
// Layout.fillWidth: true
// }
// }
// // Summary
// NText {
// id: summaryText
// text: model.summary || I18n.tr("general.no-summary")
// pointSize: Style.fontSizeM
// font.weight: Font.Medium
// color: Color.mOnSurface
// textFormat: Text.PlainText
// wrapMode: Text.Wrap
// Layout.fillWidth: true
// maximumLineCount: isExpanded ? 999 : 2
// elide: Text.ElideRight
// }
// // Body
// NText {
// id: bodyText
// text: model.body || ""
// pointSize: Style.fontSizeS
// color: Color.mOnSurfaceVariant
// textFormat: Text.PlainText
// wrapMode: Text.Wrap
// Layout.fillWidth: true
// maximumLineCount: isExpanded ? 999 : 3
// elide: Text.ElideRight
// visible: text.length > 0
// }
// // Spacer for expand indicator
// Item {
// Layout.fillWidth: true
// Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0
// }
// // Expand indicator
// RowLayout {
// Layout.fillWidth: true
// visible: !isExpanded && (summaryText.truncated || bodyText.truncated)
// spacing: Style.marginXS
// Item {
// Layout.fillWidth: true
// }
// NText {
// text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand"
// pointSize: Style.fontSizeXS
// color: Color.mPrimary
// font.weight: Font.Medium
// }
// NIcon {
// icon: "chevron-down"
// pointSize: Style.fontSizeS
// color: Color.mPrimary
// }
// }
// }
// // Delete button
// NIconButton {
// icon: "trash"
// tooltipText: I18n.tr("tooltips.delete-notification")
// baseSize: Style.baseWidgetSize * 0.7
// Layout.alignment: Qt.AlignTop
// onClicked: {
// // Remove from history using the service API
// NotificationService.removeFromHistory(notificationId)
// }
// }
// }
// }
// }
// }
}
}
}
@@ -9,8 +9,9 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
preferredWidth: 400 * Style.uiScaleRatio
@@ -148,7 +149,7 @@ NPanel {
}
}
// Override keyboard handlers from NPanel
// Override keyboard handlers from SmartPanel
function onEscapePressed() {
if (timerActive) {
cancelTimer()
@@ -3,12 +3,13 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Modules.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
preferredWidth: 820 * Style.uiScaleRatio
@@ -284,7 +285,7 @@ NPanel {
}
}
// Override keyboard handlers from NPanel
// Override keyboard handlers from SmartPanel
function onTabPressed() {
selectNextTab()
}
@@ -7,7 +7,7 @@ import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import "../../Helpers/FuzzySort.js" as FuzzySort
import "../../../Helpers/FuzzySort.js" as FuzzySort
ColumnLayout {
id: root
@@ -6,8 +6,9 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
preferredWidth: 520 * Style.uiScaleRatio
@@ -6,13 +6,12 @@ import Quickshell.Services.SystemTray
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
// A compact grid panel listing all tray items, opened from the Tray widget
NPanel {
SmartPanel {
id: root
objectName: "trayDropdownPanel"
// Widget info for menu functionality
property string widgetSection: ""
property int widgetIndex: -1
@@ -79,7 +78,7 @@ NPanel {
preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding)
preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding)
// Positioning is handled automatically by NPanel when toggle(buttonItem) is called
// Positioning is handled automatically by SmartPanel when toggle(buttonItem) is called
// Watch for settings changes to refresh the dropdown
Connections {
@@ -177,7 +176,5 @@ NPanel {
}
}
}
// (Tray menu now uses dedicated TrayMenu via PanelService)
}
}
@@ -5,17 +5,11 @@ import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
objectName: "trayMenu"
// Avoid bouncy feel: keep fade but disable scale/slide transforms
disableScaleAnimation: true
disableSlideAnimation: false
customSlideDistance: 100
// Inputs
property QsMenuHandle menu
property var trayItem: null
@@ -41,7 +35,7 @@ NPanel {
// Only show submenu in place (replace main menu)
property bool inPlaceSubmenu: true
// Let NPanel size to our content
// Let Panel size to our content
readonly property real contentPreferredWidth: root.menuWidth
readonly property real contentPreferredHeight: {
// If showing submenu in-place, size to the submenu's flickable content height
@@ -4,12 +4,13 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Settings
import qs.Modules.Panels.Settings
import qs.Services
import qs.Widgets
import "../../Helpers/FuzzySort.js" as FuzzySort
import qs.Modules.MainScreen
import "../../../Helpers/FuzzySort.js" as FuzzySort
NPanel {
SmartPanel {
id: root
preferredWidth: 800 * Style.uiScaleRatio
@@ -6,8 +6,9 @@ import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.MainScreen
NPanel {
SmartPanel {
id: root
preferredWidth: 420 * Style.uiScaleRatio

Some files were not shown because too many files have changed in this diff Show More