Files
noctalia-shell/Widgets/NFullScreenWindow.qml
T
ItsLemmy 101b27fcc7 New windowing system
Large commit that totally refactor of the way we handle the bar and
panels.

Testing should focus on Panels, Bar, Keyboard Focus, IPC calls.

Changes brief:
- One NFullScreenWindow per screen which handle it's bar and dedicated
panels.
- Added shadows
- Reintroduced dimming
- New panels animations
- Proper Z ordering
- Panels on overlay laywer is not reimplemented, if we do it then the
bar will be on the Overlay too
- Panel dragging was not reimplemented, to be discussed before
reimplementing
- Still a WIP, need to work more on shadows and polishing + debugging.
2025-11-03 00:53:02 -05:00

609 lines
18 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
/**
* NFullScreenWindow - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
required property var barComponent
required property var panelComponents
Component.onCompleted: {
Logger.d("NFullScreenWindow", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y)
}
// Debug: Log mask region changes
onMaskChanged: {
Logger.d("NFullScreenWindow", "Mask changed!")
Logger.d("NFullScreenWindow", " Bar region:", barLoader.item?.barRegion)
Logger.d("NFullScreenWindow", " Panel count:", panelsRepeater.count)
for (var i = 0; i < panelsRepeater.count; i++) {
var panelItem = panelsRepeater.itemAt(i)?.item
Logger.d("NFullScreenWindow", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion)
}
}
// Wayland
// Always use Exclusive keyboard focus when a panel is open
// This ensures all keyboard shortcuts work reliably (Escape, etc.)
// The centralized shortcuts in this window handle delegation to panels
WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown")
anchors {
top: true
bottom: true
left: true
right: true
}
// Desktop dimming when panels are open
property bool dimDesktop: Settings.data.general.dimDesktop
property bool isPanelOpen: PanelService.openedPanel !== null
color: {
if (dimDesktop && isPanelOpen) {
return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy)
}
return Color.transparent
}
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
function updateMask() {
// Build the regions list
var regionsList = [barMaskRegion]
// Add background region if a panel is open
// This makes the background clickable (not click-through) so we can detect clicks to close panels
if (root.isPanelOpen) {
regionsList.push(backgroundMaskRegion)
}
// Add regions for each open panel
for (var i = 0; i < panelMaskRepeater.count; i++) {
var wrapperItem = panelMaskRepeater.itemAt(i)
if (wrapperItem && wrapperItem.maskRegion) {
var panelItem = wrapperItem.panelItem
if (panelItem && panelItem.isPanelOpen) {
var panelRegion = panelItem.panelRegion
// Update the mask region's coordinates from the panel's actual region
if (panelRegion) {
wrapperItem.maskRegion.x = panelRegion.x
wrapperItem.maskRegion.y = panelRegion.y
wrapperItem.maskRegion.width = panelRegion.width
wrapperItem.maskRegion.height = panelRegion.height
regionsList.push(wrapperItem.maskRegion)
}
}
}
}
// Update the mask's regions
clickableMask.regions = regionsList
}
// Listen to PanelService to update mask when panels open/close
Connections {
target: PanelService
function onWillOpen() {
root.updateMask()
}
function onDidClose() {
root.updateMask()
}
}
// Also update mask when isPanelOpen changes (defensive)
onIsPanelOpenChanged: {
Logger.d("NFullScreenWindow", "isPanelOpen changed to:", isPanelOpen)
Qt.callLater(() => root.updateMask())
}
// Background region - for closing panels when clicking outside (separate from mask)
Region {
id: backgroundMaskRegion
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Subtract
}
// Smart mask: Make everything click-through except bar and open panels
mask: Region {
id: clickableMask
// Cover entire window (everything is masked/click-through)
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Xor
// Regions list is set programmatically in updateMask()
// Initially just the bar
regions: [barMaskRegion]
// Bar region - subtract bar area from mask
Region {
id: barMaskRegion
property var barRegion: barLoader.item && barLoader.item.barRegion ? barLoader.item.barRegion : null
x: barRegion ? barRegion.x : 0
y: barRegion ? barRegion.y : 0
width: barRegion ? barRegion.width : 0
height: barRegion ? barRegion.height : 0
intersection: Intersection.Subtract
}
}
// Container for panel mask regions (created dynamically)
Item {
id: panelMaskRegions
// Create a Region for each panel
Repeater {
id: panelMaskRepeater
model: panelsRepeater.count
delegate: Item {
required property int index
property var panelItem: panelsRepeater.itemAt(index)?.item
property var region: panelItem && panelItem.panelRegion ? panelItem.panelRegion : null
// The actual mask region as a child
property alias maskRegion: panelMask
Region {
id: panelMask
// Coordinates are set programmatically in updateMask()
intersection: Intersection.Subtract
}
}
}
}
// Container for all UI elements
Item {
id: container
width: root.width
height: root.height
// Screen corners (integrated to avoid separate PanelWindow)
// Always positioned at actual screen edges
Loader {
id: screenCornersLoader
active: Settings.data.general.showScreenCorners && (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity >= 1 || Settings.data.bar.floating)
anchors.fill: parent
z: 1000 // Very high z-index to be on top of everything
sourceComponent: Item {
id: cornersRoot
anchors.fill: parent
property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
property real cornerRadius: Style.screenRadius
property real cornerSize: Style.screenRadius
// Top-left concave corner
Canvas {
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, height, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Top-right concave corner
Canvas {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, height, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Bottom-left concave corner
Canvas {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Bottom-right concave corner
Canvas {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
width: cornersRoot.cornerSize
height: cornersRoot.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a)
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Repaint all corners when color or radius changes
onCornerColorChanged: {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
if (topRightCorner.available)
topRightCorner.requestPaint()
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
onCornerRadiusChanged: {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
if (topRightCorner.available)
topRightCorner.requestPaint()
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
}
}
// Background MouseArea for closing panels when clicking outside
// Active whenever a panel is open - the mask ensures it only receives clicks when panel is open
MouseArea {
anchors.fill: parent
enabled: root.isPanelOpen
onClicked: {
if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
z: 0 // Behind panels and bar
}
// All panels (as Items, not PanelWindows)
Repeater {
id: panelsRepeater
model: root.panelComponents
delegate: Loader {
id: panelLoader
// Lazy load panels - only create when first requested
// Panel stays loaded once created for faster subsequent opens
active: false
asynchronous: false
sourceComponent: modelData.component
// Fill the container so panels have proper parent dimensions
anchors.fill: parent
// Panel properties binding
property var panelScreen: root.screen
property string panelId: modelData.id
property int panelZIndex: modelData.zIndex || 50
property bool hasBeenRequested: false
Component.onCompleted: {
// Register the loader immediately so PanelService can load it on-demand
var objectName = panelId + "-" + (panelScreen?.name || "unknown")
PanelService.registerPanelLoader(panelLoader, objectName)
}
// Activate loader when panel is first requested
function ensureLoaded() {
if (!hasBeenRequested) {
Logger.d("NFullScreenWindow", "Loading panel on-demand:", panelId)
hasBeenRequested = true
active = true
}
}
onLoaded: {
if (item) {
// Set unique objectName per screen BEFORE registration: "calendarPanel-DP-1"
item.objectName = panelId + "-" + (panelScreen?.name || "unknown")
// Set z-order for panels
item.z = panelZIndex
item.screen = panelScreen
// Now register with PanelService (after objectName is set)
PanelService.registerPanel(item)
Logger.d("NFullScreenWindow", "Panel loaded with objectName:", item.objectName, "on screen:", panelScreen?.name)
}
}
}
}
// Bar (always on top)
Loader {
id: barLoader
asynchronous: false
sourceComponent: root.barComponent
// Fill parent to provide dimensions for Bar to reference
anchors.fill: parent
property ShellScreen screen: root.screen
onLoaded: {
Logger.d("NFullScreenWindow", "Bar loaded:", item !== null)
if (item) {
Logger.d("NFullScreenWindow", "Bar size:", item.width, "x", item.height)
// Bar always has highest z-index
item.z = 100
// Bind screen to bar component (use binding for reactivity)
item.screen = Qt.binding(function () {
return barLoader.screen
})
Logger.d("NFullScreenWindow", "Bar screen set to:", item.screen?.name)
}
}
}
}
// 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()
}
}
}
}