Files
noctalia-shell/Modules/MainScreen/MainScreen.qml
T

691 lines
25 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import "Backgrounds" as Backgrounds
import qs.Commons
// All panels
import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Audio
import qs.Modules.Panels.Battery
import qs.Modules.Panels.Bluetooth
import qs.Modules.Panels.Brightness
import qs.Modules.Panels.Changelog
import qs.Modules.Panels.Clock
import qs.Modules.Panels.ControlCenter
import qs.Modules.Panels.Dock
import qs.Modules.Panels.Launcher
import qs.Modules.Panels.Media
import qs.Modules.Panels.Network
import qs.Modules.Panels.NotificationHistory
import qs.Modules.Panels.Plugins
import qs.Modules.Panels.SessionMenu
import qs.Modules.Panels.Settings
import qs.Modules.Panels.SetupWizard
import qs.Modules.Panels.SystemStats
import qs.Modules.Panels.Tray
import qs.Modules.Panels.Wallpaper
import qs.Services.Compositor
import qs.Services.Power
import qs.Services.UI
/**
* MainScreen - Single PanelWindow per screen that manages all panels and the bar
*/
PanelWindow {
id: root
Component.onCompleted: {
Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y);
}
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-background-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that
WlrLayershell.keyboardFocus: {
// No panel open anywhere: no keyboard focus needed
if (!root.isAnyPanelOpen) {
return WlrKeyboardFocus.None;
}
// Panel open on THIS screen: use panel's preferred focus mode
if (root.isPanelOpen) {
// Hyprland's Exclusive captures ALL input globally (including pointer),
// preventing click-to-close from working on other monitors.
// Workaround: briefly use Exclusive when panel opens (for text input focus),
// then switch to OnDemand (for click-to-close on other screens).
if (CompositorService.isHyprland) {
return PanelService.isInitializingKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand;
}
return PanelService.openedPanel.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand;
}
// Panel open on ANOTHER screen: OnDemand allows receiving pointer events for click-to-close
return WlrKeyboardFocus.OnDemand;
}
anchors {
top: true
bottom: true
left: true
right: true
}
// Desktop dimming when panels are open
property real dimmerOpacity: Settings.data.general.dimmerOpacity ?? 0.8
property bool isPanelOpen: (PanelService.openedPanel !== null) && (PanelService.openedPanel.screen === screen)
property bool isPanelClosing: (PanelService.openedPanel !== null) && PanelService.openedPanel.isClosing
property bool isAnyPanelOpen: PanelService.openedPanel !== null
color: {
if (dimmerOpacity > 0 && isPanelOpen && !isPanelClosing) {
return Qt.alpha(Color.mShadow, dimmerOpacity);
}
return "transparent";
}
Behavior on color {
enabled: !PanelService.closedImmediately
ColorAnimation {
duration: isPanelClosing ? Style.animationFaster : Style.animationNormal
easing.type: Easing.OutQuad
}
}
// Reset closedImmediately flag after color change is applied
onColorChanged: {
if (PanelService.closedImmediately) {
PanelService.closedImmediately = false;
}
}
// Check if bar should be visible on this screen
readonly property bool barShouldShow: {
// Check global bar visibility (includes overview state)
if (!BarService.effectivelyVisible)
return false;
// Check screen-specific configuration
var monitors = Settings.data.bar.monitors || [];
var screenName = screen?.name || "";
// If no monitors specified, show on all screens
// If monitors specified, only show if this screen is in the list
return monitors.length === 0 || monitors.includes(screenName);
}
// Make everything click-through except bar
mask: Region {
id: clickableMask
// Cover entire window (everything is masked/click-through)
x: 0
y: 0
width: root.width
height: root.height
intersection: Intersection.Xor
// Only include regions that are actually needed
// panelRegions is handled by PanelService, bar is local to this screen
regions: [barMaskRegion, backgroundMaskRegion]
// Bar region - subtract bar area from mask (only if bar should be shown on this screen)
Region {
id: barMaskRegion
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real barThickness: Style.barHeight
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property string barPos: Settings.data.bar.position || "top"
// Bar / Frame Mask
Region {
// Mode: Simple or Floating
x: barPlaceholder.x
y: barPlaceholder.y
width: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.width : 0
height: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.height : 0
intersection: Intersection.Subtract
}
// Mode: Framed - 4 sides
Region {
// Top side
Region {
x: 0
y: 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "top" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
intersection: Intersection.Subtract
}
// Bottom side
Region {
x: 0
y: (barMaskRegion.isFramed && root.barShouldShow) ? (root.height - (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
intersection: Intersection.Subtract
}
// Left side
Region {
x: 0
y: 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "left" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0
intersection: Intersection.Subtract
}
// Right side
Region {
x: (barMaskRegion.isFramed && root.barShouldShow) ? (root.width - (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0
width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0
height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0
intersection: Intersection.Subtract
}
}
}
// Background region for click-to-close - reactive sizing
// Uses isAnyPanelOpen so clicking on any screen's background closes the panel
Region {
id: backgroundMaskRegion
x: 0
y: 0
width: root.isAnyPanelOpen ? root.width : 0
height: root.isAnyPanelOpen ? root.height : 0
intersection: Intersection.Subtract
}
}
// Blur behind the bar and open panels — attached to PanelWindow (required by BackgroundEffect API)
BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? blurRegion : null
Region {
id: blurRegion
// ── Non-framed bar (simple/floating): single rectangle with bar corner states ──
Region {
x: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.x : 0
y: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.y : 0
width: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.width : 0
height: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.height : 0
radius: Style.radiusL
topLeftCorner: barPlaceholder.topLeftCornerState
topRightCorner: barPlaceholder.topRightCornerState
bottomLeftCorner: barPlaceholder.bottomLeftCornerState
bottomRightCorner: barPlaceholder.bottomRightCornerState
}
// ── Framed bar: full screen minus rounded hole ──
Region {
x: 0
y: 0
width: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.width : 0
height: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.height : 0
Region {
intersection: Intersection.Subtract
x: backgroundBlur.frameHoleX
y: backgroundBlur.frameHoleY
width: backgroundBlur.frameHoleX2 - backgroundBlur.frameHoleX
height: backgroundBlur.frameHoleY2 - backgroundBlur.frameHoleY
radius: backgroundBlur.frameR
}
}
// ── Panel blur regions ──
// Opening panel
Region {
x: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.x) : 0
y: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.y) : 0
width: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.width) : 0
height: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.height) : 0
radius: Style.radiusL
topLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topLeftCornerState : CornerState.Normal
topRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topRightCornerState : CornerState.Normal
bottomLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomLeftCornerState : CornerState.Normal
bottomRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomRightCornerState : CornerState.Normal
}
// Closing panel (coexists with opening panel during transition)
Region {
x: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.x) : 0
y: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.y) : 0
width: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.width) : 0
height: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.height) : 0
radius: Style.radiusL
topLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topLeftCornerState : CornerState.Normal
topRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topRightCornerState : CornerState.Normal
bottomLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomLeftCornerState : CornerState.Normal
bottomRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomRightCornerState : CornerState.Normal
}
}
// --------------------------------------
// Container for all UI elements
Item {
id: container
width: root.width
height: root.height
// Unified backgrounds container / unified shadow system
// Renders all bar and panel backgrounds as ShapePaths within a single Shape
// This allows the shadow effect to apply to all backgrounds in one render pass
Backgrounds.AllBackgrounds {
id: unifiedBackgrounds
anchors.fill: parent
bar: barPlaceholder.barItem || null
windowRoot: root
z: 0 // Behind all content
}
// Background MouseArea for closing panels when clicking outside
// Uses isAnyPanelOpen so clicking on any screen's background closes the panel
MouseArea {
anchors.fill: parent
enabled: root.isAnyPanelOpen
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (PanelService.openedPanel) {
PanelService.openedPanel.close();
}
}
z: 0 // Behind panels and bar
}
// ---------------------------------------
// All panels always exist
// ---------------------------------------
AudioPanel {
id: audioPanel
objectName: "audioPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
MediaPlayerPanel {
id: mediaPlayerPanel
objectName: "mediaPlayerPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BatteryPanel {
id: batteryPanel
objectName: "batteryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BluetoothPanel {
id: bluetoothPanel
objectName: "bluetoothPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
BrightnessPanel {
id: brightnessPanel
objectName: "brightnessPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ControlCenterPanel {
id: controlCenterPanel
objectName: "controlCenterPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ChangelogPanel {
id: changelogPanel
objectName: "changelogPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
ClockPanel {
id: clockPanel
objectName: "clockPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
Launcher {
id: launcherPanel
objectName: "launcherPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
NotificationHistoryPanel {
id: notificationHistoryPanel
objectName: "notificationHistoryPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SessionMenu {
id: sessionMenuPanel
objectName: "sessionMenuPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SettingsPanel {
id: settingsPanel
objectName: "settingsPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SetupWizard {
id: setupWizardPanel
objectName: "setupWizardPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
TrayDrawerPanel {
id: trayDrawerPanel
objectName: "trayDrawerPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
NetworkPanel {
id: networkPanel
objectName: "networkPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
SystemStatsPanel {
id: systemStatsPanel
objectName: "systemStatsPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
StaticDockPanel {
id: staticDockPanel
objectName: "staticDockPanel-" + (root.screen?.name || "unknown")
screen: root.screen
}
// ----------------------------------------------
// Plugin panel slots
// ----------------------------------------------
PluginPanelSlot {
id: pluginPanel1
objectName: "pluginPanel1-" + (root.screen?.name || "unknown")
screen: root.screen
slotNumber: 1
}
PluginPanelSlot {
id: pluginPanel2
objectName: "pluginPanel2-" + (root.screen?.name || "unknown")
screen: root.screen
slotNumber: 2
}
// ----------------------------------------------
// Bar background placeholder - just for background positioning (actual bar content is in BarContentWindow)
Item {
id: barPlaceholder
// Expose self as barItem for AllBackgrounds compatibility
readonly property var barItem: barPlaceholder
// Screen reference
property ShellScreen screen: root.screen
// Bar background positioning properties (per-screen)
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property bool isFramed: Settings.data.bar.barType === "framed"
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12
readonly property bool barFloating: Settings.data.bar.barType === "floating"
readonly property real barMarginH: barFloating ? Math.floor(Settings.data.bar.marginHorizontal) : 0
readonly property real barMarginV: barFloating ? Math.floor(Settings.data.bar.marginVertical) : 0
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
// Auto-hide properties (read by AllBackgrounds for background fade)
readonly property bool autoHide: Settings.getBarDisplayModeForScreen(screen?.name) === "auto_hide"
property bool isHidden: autoHide
Connections {
target: BarService
function onBarAutoHideStateChanged(screenName, hidden) {
if (screenName === barPlaceholder.screen?.name) {
barPlaceholder.isHidden = hidden;
}
}
}
// Expose bar dimensions directly on this Item for BarBackground
// Use screen dimensions directly
x: {
if (barPosition === "right")
return (screen?.width ?? 0) - barHeight - barMarginH;
if (isFramed && !barIsVertical)
return frameThickness;
return barMarginH;
}
y: {
if (barPosition === "bottom")
return (screen?.height ?? 0) - barHeight - barMarginV;
if (isFramed && barIsVertical)
return frameThickness;
return barMarginV;
}
width: {
if (barIsVertical) {
return barHeight;
}
if (isFramed)
return (screen?.width ?? 0) - frameThickness * 2;
return (screen?.width ?? 0) - barMarginH * 2;
}
height: {
if (!barIsVertical) {
return barHeight;
}
if (isFramed)
return (screen?.height ?? 0) - frameThickness * 2;
return (screen?.height ?? 0) - barMarginV * 2;
}
// Corner states (same as Bar.qml)
readonly property int topLeftCornerState: {
if (barFloating)
return 0;
if (barPosition === "top")
return -1;
if (barPosition === "left")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int topRightCornerState: {
if (barFloating)
return 0;
if (barPosition === "top")
return -1;
if (barPosition === "right")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int bottomLeftCornerState: {
if (barFloating)
return 0;
if (barPosition === "bottom")
return -1;
if (barPosition === "left")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
readonly property int bottomRightCornerState: {
if (barFloating)
return 0;
if (barPosition === "bottom")
return -1;
if (barPosition === "right")
return -1;
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
return barIsVertical ? 1 : 2;
}
return -1;
}
}
// Screen Corners
ScreenCorners {}
// Blur behind the bar and open panels
// Helper object holding computed properties for blur regions
QtObject {
id: backgroundBlur
// Panel background geometry (from the currently open panel on this screen)
readonly property var panelBg: {
var op = PanelService.openedPanel;
if (!op || op.screen !== root.screen || op.blurEnabled === false)
return null;
var region = op.panelRegion;
return (region && region.visible) ? region.panelItem : null;
}
// Panel background geometry for the closing panel (may coexist with panelBg)
readonly property var closingPanelBg: {
var cp = PanelService.closingPanel;
if (!cp || cp.screen !== root.screen || cp.blurEnabled === false)
return null;
var region = cp.panelRegion;
return (region && region.visible) ? region.panelItem : null;
}
// Framed bar: inner hole boundary (where the hole begins on each axis)
// These are the x/y coordinates of the 4 inner hole corners
readonly property real frameHoleX: barPlaceholder.barPosition === "left" ? barPlaceholder.barHeight : barPlaceholder.frameThickness
readonly property real frameHoleY: barPlaceholder.barPosition === "top" ? barPlaceholder.barHeight : barPlaceholder.frameThickness
readonly property real frameHoleX2: root.width - (barPlaceholder.barPosition === "right" ? barPlaceholder.barHeight : barPlaceholder.frameThickness)
readonly property real frameHoleY2: root.height - (barPlaceholder.barPosition === "bottom" ? barPlaceholder.barHeight : barPlaceholder.frameThickness)
readonly property real frameR: Settings.data.bar.frameRadius ?? 20
}
// Native idle inhibitor — one per active MainScreen window.
// Multiple inhibitors bound to the same enabled state are harmless;
// having one per screen is more robust than picking a "primary" screen.
IdleInhibitor {
window: root
enabled: IdleInhibitorService.isInhibited
Component.onCompleted: {
IdleInhibitorService.nativeInhibitorAvailable = true;
Logger.d("IdleInhibitor", "Native IdleInhibitor active on screen:", root.screen?.name);
}
}
}
// Centralized Keyboard Shortcuts
// These shortcuts delegate to the opened panel's handler functions
// Panels can implement: onEscapePressed, onTabPressed, onBackTabPressed,
// onUpPressed, onDownPressed, onReturnPressed, etc...
Instantiator {
model: Settings.data.general.keybinds.keyEscape || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onEscapePressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onEscapePressed()
}
}
Shortcut {
sequence: "Tab"
enabled: root.isPanelOpen && (PanelService.openedPanel.onTabPressed !== undefined)
onActivated: PanelService.openedPanel.onTabPressed()
}
Shortcut {
sequence: "Backtab"
enabled: root.isPanelOpen && (PanelService.openedPanel.onBackTabPressed !== undefined)
onActivated: PanelService.openedPanel.onBackTabPressed()
}
Instantiator {
model: Settings.data.general.keybinds.keyUp || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onUpPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onUpPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyDown || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onDownPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onDownPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyEnter || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onEnterPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onEnterPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyLeft || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onLeftPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onLeftPressed()
}
}
Instantiator {
model: Settings.data.general.keybinds.keyRight || []
Shortcut {
sequence: modelData
enabled: root.isPanelOpen && (PanelService.openedPanel.onRightPressed !== undefined) && !PanelService.isKeybindRecording
onActivated: PanelService.openedPanel.onRightPressed()
}
}
Shortcut {
sequence: "Home"
enabled: root.isPanelOpen && (PanelService.openedPanel.onHomePressed !== undefined)
onActivated: PanelService.openedPanel.onHomePressed()
}
Shortcut {
sequence: "End"
enabled: root.isPanelOpen && (PanelService.openedPanel.onEndPressed !== undefined)
onActivated: PanelService.openedPanel.onEndPressed()
}
Shortcut {
sequence: "PgUp"
enabled: root.isPanelOpen && (PanelService.openedPanel.onPageUpPressed !== undefined)
onActivated: PanelService.openedPanel.onPageUpPressed()
}
Shortcut {
sequence: "PgDown"
enabled: root.isPanelOpen && (PanelService.openedPanel.onPageDownPressed !== undefined)
onActivated: PanelService.openedPanel.onPageDownPressed()
}
}