mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
630 lines
19 KiB
QML
630 lines
19 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
|
|
|
|
// Shadow properties
|
|
property real shadowOpacity: 1.0
|
|
property real shadowBlur: 1.0
|
|
property real shadowHorizontalOffset: 2
|
|
property real shadowVerticalOffset: 3
|
|
|
|
property color black: "#000000"
|
|
property color white: "#ffffff"
|
|
|
|
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: 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
|
|
}
|
|
}
|
|
|
|
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
|
|
// Only include panels that are open AND not closing (to allow click-through during close animation)
|
|
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 && !panelItem.isClosing) {
|
|
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() {
|
|
// Delay mask update to ensure panel's isPanelOpen is updated first
|
|
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
|
|
|
|
// Update mask when bar geometry changes
|
|
onWidthChanged: Qt.callLater(() => root.updateMask())
|
|
onHeightChanged: Qt.callLater(() => root.updateMask())
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// Apply shadow effect
|
|
layer.enabled: Settings.data.general.enableShadows
|
|
layer.smooth: true
|
|
// layer.textureSize: {
|
|
// var dpr = CompositorService.getDisplayScale(screen.name)
|
|
// return Qt.size(width * dpr, height * dpr)
|
|
// }
|
|
layer.effect: MultiEffect {
|
|
shadowEnabled: true
|
|
shadowOpacity: root.shadowOpacity
|
|
shadowHorizontalOffset: root.shadowHorizontalOffset
|
|
shadowVerticalOffset: root.shadowVerticalOffset
|
|
shadowColor: black
|
|
blur: root.shadowBlur
|
|
blurMax: 32
|
|
}
|
|
|
|
// Screen corners (integrated to avoid separate PanelWindow)
|
|
// Always positioned at actual screen edges
|
|
Loader {
|
|
id: screenCornersLoader
|
|
active: Settings.data.general.showScreenCorners
|
|
|
|
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 ? black : 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 = white
|
|
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 = white
|
|
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 = white
|
|
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 = white
|
|
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")
|
|
item.screen = panelScreen
|
|
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
|
|
// 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("NFullScreenWindow", "Bar loaded:", item !== null)
|
|
if (item) {
|
|
Logger.d("NFullScreenWindow", "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()
|
|
}
|
|
}
|
|
}
|
|
}
|