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.
This commit is contained in:
ItsLemmy
2025-11-03 00:53:02 -05:00
parent 98ed4ec450
commit 101b27fcc7
62 changed files with 2727 additions and 1496 deletions

View File

@@ -226,13 +226,17 @@
},
"floating": {
"label": "Schwebende Statusleiste",
"description": "Statusleiste als schwebende 'Pille' anzeigen. Hinweis: Dies verschiebt die Bildschirmecken an die Ränder."
"description": "Statusleiste als schwebende 'Pille' anzeigen."
},
"margins": {
"label": "Ränder",
"description": "Ränder um die schwebende Statusleiste anpassen.",
"vertical": "Vertikal",
"horizontal": "Horizontal"
},
"outer-corners": {
"description": "Zeigt nach außen gewölbte Ecken auf der Leiste an.",
"label": "Äußere Ecken"
}
},
"widgets": {
@@ -584,6 +588,10 @@
"foot": {
"description": "Schreibt {filepath} und lädt neu",
"description-missing": "Erfordert {app} Terminal"
},
"alacritty": {
"description": "Schreibe {Dateipfad} und lade neu",
"description-missing": "Benötigt die Installation von {app}"
}
},
"programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": {
"description": "Wenn aktiviert, werden die Panels mit einem schönen, umgekehrten Eckdesign an der Leiste befestigt.",
"label": "Paneele an Stange befestigen"
},
"dim-desktop": {
"description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.",
"label": "Dimmer Schreibtisch"
}
},
"lock-screen": {

View File

@@ -226,7 +226,11 @@
},
"floating": {
"label": "Floating bar",
"description": "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges."
"description": "Displays the bar as a floating 'pill'."
},
"outer-corners": {
"label": "Outer corners",
"description": "Displays outwardly curved corners on the bar."
},
"margins": {
"label": "Margins",
@@ -577,6 +581,10 @@
"terminal": {
"label": "Terminal",
"description": "Terminal emulator theming.",
"alacritty": {
"description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed"
},
"kitty": {
"description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed"
@@ -851,6 +859,10 @@
"description": "Changes the size of the general user interface, excluding the bar.",
"reset-scaling": "Reset interface scaling"
},
"dim-desktop": {
"label": "Dim desktop",
"description": "Dim the desktop when panels or menus are open."
},
"border-radius": {
"label": "Border radius",
"description": "Controls the corner roundness of windows, buttons, and other elements.",

View File

@@ -226,13 +226,17 @@
},
"floating": {
"label": "Barra flotante",
"description": "Muestra la barra como una 'píldora' flotante. Nota: Esto moverá las esquinas de la pantalla a los bordes."
"description": "Muestra la barra como una 'píldora' flotante."
},
"margins": {
"label": "Márgenes",
"description": "Ajusta los márgenes alrededor de la barra flotante.",
"vertical": "Vertical",
"horizontal": "Horizontal"
},
"outer-corners": {
"description": "Muestra esquinas curvadas hacia afuera en la barra.",
"label": "Esquinas exteriores"
}
},
"widgets": {
@@ -584,6 +588,10 @@
"foot": {
"description": "Escribir {filepath} y recargar",
"description-missing": "Requiere que {app} esté instalado"
},
"alacritty": {
"description": "Escribe {filepath} y recarga",
"description-missing": "Requiere que {app} esté instalado/a."
}
},
"programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": {
"description": "Cuando está habilitado, los paneles se adjuntarán a la barra con un hermoso diseño de esquina invertida.",
"label": "Adjuntar paneles a la barra"
},
"dim-desktop": {
"description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.",
"label": "Dim escritorio"
}
},
"lock-screen": {

View File

@@ -226,13 +226,17 @@
},
"floating": {
"label": "Barre flottante",
"description": "Affiche la barre sous forme de 'pilule' flottante. Note : Ceci déplacera les coins de l'écran vers les bords."
"description": "Affiche la barre sous forme de 'pilule' flottante."
},
"margins": {
"label": "Marges",
"description": "Ajustez les marges autour de la barre flottante.",
"vertical": "Verticale",
"horizontal": "Horizontale"
},
"outer-corners": {
"description": "Affiche des coins incurvés vers l'extérieur sur la barre.",
"label": "Coins extérieurs"
}
},
"widgets": {
@@ -584,6 +588,10 @@
"foot": {
"description": "Écrire ~/.config/foot/themes/noctalia et recharger",
"description-missing": "Nécessite que le terminal foot soit installé"
},
"alacritty": {
"description": "Écrire {filepath} et recharger.",
"description-missing": "Nécessite l'installation de {app}"
}
},
"programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": {
"description": "Lorsque cette option est activée, les panneaux seront attachés à la barre avec un design élégant de coin inversé.",
"label": "Fixer les panneaux à la barre."
},
"dim-desktop": {
"description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.",
"label": "Dim bureau"
}
},
"lock-screen": {

View File

@@ -226,13 +226,17 @@
},
"floating": {
"label": "Barra flutuante",
"description": "Exibe a barra como uma 'pílula' flutuante. Nota: Isso moverá os cantos da tela para as bordas."
"description": "Exibe a barra como uma 'pílula' flutuante."
},
"margins": {
"label": "Margens",
"description": "Ajuste as margens ao redor da barra flutuante.",
"vertical": "Vertical",
"horizontal": "Horizontal"
},
"outer-corners": {
"description": "Exibe cantos curvados para fora na barra.",
"label": "Cantos externos"
}
},
"widgets": {
@@ -546,6 +550,10 @@
"foot": {
"description": "Escrever {filepath} e recarregar",
"description-missing": "Requer que o {app} esteja instalado"
},
"alacritty": {
"description": "Escreva {filepath} e recarregue.",
"description-missing": "Requer que o {app} esteja instalado."
}
},
"programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": {
"description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.",
"label": "Anexar painéis à barra"
},
"dim-desktop": {
"description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.",
"label": "Dim área de trabalho"
}
},
"lock-screen": {

View File

@@ -226,13 +226,17 @@
},
"floating": {
"label": "浮动状态栏",
"description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。"
"description": "将工具栏显示为浮动的“药丸”形状。"
},
"margins": {
"label": "边距",
"description": "调整浮动状态栏周围的边距。",
"vertical": "垂直",
"horizontal": "水平"
},
"outer-corners": {
"description": "在栏上显示向外弯曲的角。",
"label": "外角"
}
},
"widgets": {
@@ -584,6 +588,10 @@
"foot": {
"description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}"
},
"alacritty": {
"description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}"
}
},
"programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": {
"description": "启用后,面板将以美观的倒角设计附加到栏上。",
"label": "将面板连接到杆上"
},
"dim-desktop": {
"description": "当面板或菜单打开时,桌面变暗。",
"label": "昏暗的桌面"
}
},
"lock-screen": {

366
CLAUDE.md Normal file
View File

@@ -0,0 +1,366 @@
# Noctalia Shell
**A beautiful, minimal desktop shell for Wayland that actually gets out of your way.**
Noctalia is a desktop shell built on Quickshell (Qt/QML framework) with a warm lavender aesthetic. It provides a complete desktop environment experience with panels, dock, notifications, lock screen, and extensive customization options.
## AI Guidance
* After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action.
* For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
* Before you finish, please verify your solution
* Do what has been asked; nothing more, nothing less.
* NEVER create files unless they're absolutely necessary for achieving your goal.
* ALWAYS prefer editing an existing file to creating a new one.
* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
## Project Overview
- **Primary Language**: QML (Qt Quick)
- **Framework**: Quickshell (Wayland-native shell framework)
- **Supported Compositors**: Niri, Hyprland, Sway (with support for other Wayland compositors)
- **License**: MIT
- **Design Philosophy**: "quiet by design" - minimal, non-intrusive UI
## Architecture
### Core Entry Point
- [shell.qml](shell.qml) - Main shell root that orchestrates all components
- Initializes services in a specific order
- Manages screen-specific instances of bars and panels
- Uses lazy loading with QML Loaders for memory optimization
- Implements NFullScreenWindow for each screen to manage bar + panels
### Directory Structure
#### `/Modules/` - UI Components
Core visual modules and panels:
- **Bar/** - Top/bottom bar with multiple widgets
- Audio, Bluetooth, Battery, Calendar, WiFi submodules
- Extras for additional bar functionality
- **ControlCenter/** - Quick settings panel
- Cards/ - MediaCard, ShortcutsCard, etc.
- Widgets/ - WiFi, Bluetooth, NightLight, PowerProfile, KeepAwake, ScreenRecorder, Notifications, WallpaperSelector
- **Dock/** - Application dock/launcher
- **Launcher/** - Application launcher/search
- **LockScreen/** - Screen locking functionality
- **Notification/** - Notification system and history
- **OSD/** - On-screen display for volume, brightness, etc.
- **Settings/** - Shell configuration UI
- **SetupWizard/** - First-run setup experience
- **SessionMenu/** - Power menu (logout, shutdown, etc.)
- **Toast/** - Toast notifications
- **Tooltip/** - Tooltip system
- **Wallpaper/** - Wallpaper management
- **Background/** - Background/wallpaper rendering
- **Audio/** - Audio visualizations (MirroredSpectrum, WaveSpectrum, LinearSpectrum)
#### `/Services/` - Business Logic
Core services that power the shell (40+ services):
**System Integration:**
- `CompositorService.qml` - Compositor-agnostic API
- `HyprlandService.qml` - Hyprland-specific integration
- `NiriService.qml` - Niri-specific integration
- `SwayService.qml` - Sway-specific integration
- `IPCService.qml` - Inter-process communication
**Hardware & System:**
- `AudioService.qml` - Audio control and monitoring
- `BatteryService.qml` - Battery status and management
- `BluetoothService.qml` - Bluetooth device management
- `BrightnessService.qml` - Screen brightness control
- `NetworkService.qml` - Network connection management
- `PowerProfileService.qml` - Power profile management
- `SystemStatService.qml` - System resource monitoring
**UI & Theming:**
- `AppThemeService.qml` - Application theming engine
- `ColorSchemeService.qml` - Color scheme management
- `DarkModeService.qml` - Dark/light mode switching
- `FontService.qml` - Font management
- `WallpaperService.qml` - Wallpaper handling (with Matugen integration)
- `MatugenTemplates.qml` - Material You color generation templates
- `NightLightService.qml` - Blue light filter
**Features:**
- `NotificationService.qml` - Notification daemon
- `MediaService.qml` - Media player control (MPRIS)
- `CalendarService.qml` - Calendar integration
- `ClipboardService.qml` - Clipboard management
- `LocationService.qml` - Geolocation for weather, etc.
- `ScreenRecorderService.qml` - Screen recording functionality
- `IdleInhibitorService.qml` - Prevent screen idle/sleep
- `KeyboardLayoutService.qml` - Keyboard layout switching
- `LockKeysService.qml` - Caps/Num lock status
**Infrastructure:**
- `BarService.qml` - Bar visibility and state management
- `BarWidgetRegistry.qml` - Registry for bar widgets
- `ControlCenterWidgetRegistry.qml` - Registry for control center widgets
- `PanelService.qml` - Panel state management
- `ToastService.qml` - Toast notification service
- `TooltipService.qml` - Tooltip service
- `HooksService.qml` - Custom hook execution
- `ProgramCheckerService.qml` - Check for installed programs
- `DistroService.qml` - Linux distribution detection
- `GitHubService.qml` - GitHub API integration
- `UpdateService.qml` - Update checking
- `CavaService.qml` - Audio visualization (Cava integration)
#### `/Commons/` - Shared Utilities
Common components used throughout the shell:
- `Settings.qml` - Centralized settings management
- `I18n.qml` - Internationalization/translations
- `Color.qml` - Color utilities and helpers
- `Icons.qml` - Icon management
- `TablerIcons.qml` - Tabler icon set (207KB icon definitions)
- `ThemeIcons.qml` - Theme-specific icons
- `Logger.qml` - Logging utility
- `Style.qml` - Shared styling definitions
- `Time.qml` - Time utilities
- `KeyboardLayout.qml` - Keyboard layout definitions
#### `/Widgets/` - Reusable UI Components
40+ custom QML widgets with the "N" prefix (Noctalia):
- **Layout**: NBox, NPanel, NScrollView, NListView, NDivider
- **Input**: NButton, NIconButton, NIconButtonHot, NTextInput, NSlider, NSpinBox, NToggle, NCheckbox, NRadioButton, NComboBox, NSearchableComboBox
- **Display**: NLabel, NText, NIcon, NHeader, NImageCached, NImageCircled, NImageRounded
- **Dialogs**: NColorPickerDialog, NFilePicker
- **Special**: NContextMenu, NColorPicker, NIconPicker, NCircleStat, NCollapsible, NSectionEditor, NReorderCheckboxes, NDateTimeTokens, NBusyIndicator, NShapedRectangle
- **System**: NFullScreenWindow, BarExclusionZone
#### `/Helpers/` - JavaScript Utilities
Helper JavaScript modules:
- `AdvancedMath.js` - Advanced mathematical functions
- `ColorsConvert.js` - Color conversion utilities
- `FuzzySort.js` - Fuzzy search implementation
- `QtObj2JS.js` - Qt object to JavaScript conversion
- `sha256.js` - SHA-256 hashing
- `Debug.js` - Debug utilities
#### `/Assets/` - Resources
- Screenshots, icons, logos, themes
- Default wallpapers
- Theme resources
#### `/Shaders/` - Graphics Shaders
Custom shader effects for visual polish
#### `/Bin/` - Executable Scripts
Helper scripts and utilities
## Key Features
### 1. Multi-Monitor Support
- Per-monitor bar configuration (TODO)
- Screen-specific panel instances
- Exclusion zones for proper compositor integration
### 2. Theming System
- Material You color generation (Matugen integration)
- Dark/light mode support
- Customizable color schemes
- Font customization
- Per-app theming capabilities
### 3. Compositor Integration
- Native support for Niri, Hyprland, Sway
- Compositor-agnostic service layer
- Workspace management
- Window control
### 4. Panel System
Advanced panel management via NFullScreenWindow:
- Launcher panel
- Control Center panel
- Calendar panel
- Settings panel
- Widget settings panel
- Notification history panel
- Session menu panel
- WiFi panel
- Bluetooth panel
- Audio panel
- Wallpaper panel
- Battery panel
All panels use z-index layering and component-based loading.
### 5. Customization
- Setup wizard for first-time users
- Extensive settings interface
- Widget registry system for adding custom widgets
- Hook system for custom scripts
- Reorderable UI elements
### 6. Audio Features
- Multiple visualization types (Mirrored, Wave, Linear spectrum)
- MPRIS media player integration
- Audio device switching
- Volume OSD
### 7. Notifications
- Custom notification daemon
- Notification history
- Do Not Disturb mode
- Per-app notification settings
## Development Setup
```bash
# Run the shell (requires Quickshell to be installed)
quickshell -p shell.qml
# Or use the shorthand
qs -p .
# Run with verbose output for debugging
qs -v -p shell.qml
# Code formatting and linting
qmlfmt -e -b 360 -t 2 -i 2 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
qmllint **/*.qml # Lint all QML files for syntax errors
```
### Nix/NixOS (Recommended)
```bash
# Enter development shell
nix develop
# Or use the legacy shell
nix-shell
```
The dev shell includes:
- Quickshell with required features
- Development utilities
- Required environment variables
### Package Structure
- Nix flake with NixOS and Home Manager modules
- Quickshell dependency (with X11 disabled, i3 enabled, hyprland enabled)
- App2unit integration for .desktop file management
## Configuration
Settings are managed through `Commons/Settings.qml`:
- Persistent configuration storage
- Settings versioning
- Migration handling
- Type-safe settings access
## Service Initialization Order
From [shell.qml:150-164](shell.qml#L150-L164):
1. WallpaperService
2. AppThemeService
3. ColorSchemeService
4. BarWidgetRegistry
5. LocationService
6. NightLightService
7. DarkModeService
8. FontService
9. HooksService
10. BluetoothService
11. BatteryService
12. IdleInhibitorService
13. PowerProfileService
14. DistroService
This order is critical - services depend on previously initialized services.
## Component Lifecycle
1. **Shell Root Initialization**
- Wait for I18n to load translations
- Wait for Settings to load configuration
2. **Service Initialization**
- Services initialize in dependency order
- Each service may depend on Settings, I18n, or other services
3. **Screen Components**
- NFullScreenWindow created per screen
- Bar and panel components loaded lazily
- Exclusion zones created after window loads
4. **Background Components**
- Background/wallpaper
- Overview (workspace overview)
- Screen corners
- Dock
- Notifications
- Lock screen
- Toast overlay
- OSD
## Special Patterns
### Lazy Loading
Components use QML Loaders extensively:
- `active` property controls when components load
- `asynchronous` for non-blocking loads
- Memory optimization for unused screens/panels
### Panel Management
NFullScreenWindow pattern:
- Single fullscreen window per screen
- Manages bar + all overlay panels
- Z-index based layering (panels at z-index 50)
- Component-based architecture for panels
### Registry Pattern
BarWidgetRegistry and ControlCenterWidgetRegistry:
- Centralized widget registration
- Dynamic widget loading
- Easy extension point for custom widgets
## Git Hooks
Uses `lefthook` for git hooks (see lefthook.yml)
## Community Resources
- Documentation: https://docs.noctalia.dev
- Discord: https://discord.noctalia.dev
- GitHub: https://github.com/noctalia-dev/noctalia-shell
## Contributing
See [development guidelines](https://docs.noctalia.dev/development/guideline)
## Current Work (Git Status)
- Modified: ControlCenter widgets (ShortcutsCard, WiFi)
- Recent commits focus on shadow effects and panel animations
- Working on bar shadow behavior when panels open
## Notes for AI Assistants
### Code Style
- QML component names use PascalCase
- Service names end with "Service.qml"
- Widget names start with "N" prefix (e.g., NButton, NPanel)
- JavaScript helpers in Helpers/ directory
### Common Tasks
1. **Adding a new bar widget**: Register in BarWidgetRegistry
2. **Adding a control center widget**: Register in ControlCenterWidgetRegistry
3. **Creating a service**: Follow the Service pattern, add to init order if needed
4. **Modifying theming**: Check AppThemeService and ColorSchemeService
5. **Panel work**: Edit in Modules/, ensure proper z-index in shell.qml
### Important Files to Check
- Settings schema: `Commons/Settings.qml`
- Service initialization: `shell.qml` (Component.onCompleted)
- Panel registration: `shell.qml` (panelComponents array)
- Theme system: `Services/AppThemeService.qml`
- Color generation: `Services/MatugenTemplates.qml`
### Testing
- Test on target compositors: Niri, Hyprland, Sway
- Check multi-monitor scenarios
- Verify lazy loading doesn't break functionality
- Test settings persistence across restarts
### Debugging
- Use `Logger.qml` for logging (Logger.i, Logger.d, Logger.w, Logger.e)
- Check console output for service initialization messages
- Verify service initialization order if adding dependencies

View File

@@ -25,27 +25,27 @@ Singleton {
}
}
// Info log (always visible)
function i(...args) {
var msg = _formatMessage(...args)
console.log(msg)
}
// Debug log (only when Settings.isDebug is true)
function d(...args) {
if (Settings && Settings.isDebug) {
var msg = _formatMessage(...args)
console.log(msg)
console.debug(msg)
}
}
// Warning log
// Info log (always visible)
function i(...args) {
var msg = _formatMessage(...args)
console.info(msg)
}
// Warning log (always visible)
function w(...args) {
var msg = _formatMessage(...args)
console.warn(msg)
}
// Error log
// Error log (always visible)
function e(...args) {
var msg = _formatMessage(...args)
console.error(msg)

View File

@@ -146,6 +146,12 @@ Singleton {
property real marginVertical: 0.25
property real marginHorizontal: 0.25
// Bar outer corners (inverted/concave corners at bar edges when not floating)
property bool outerCorners: true
// Reserves space with compositor
property bool exclusive: true
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
@@ -182,6 +188,7 @@ Singleton {
// general
property JsonObject general: JsonObject {
property string avatarImage: ""
property bool dimDesktop: true
property bool showScreenCorners: false
property bool forceBlackScreenCorners: false
property real scaleRatio: 1.0

View File

@@ -1,146 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Loader {
active: Settings.data.general.showScreenCorners && (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity >= 1 || Settings.data.bar.floating)
sourceComponent: Variants {
model: Quickshell.screens
PanelWindow {
id: root
required property ShellScreen modelData
screen: modelData
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
// Helper properties for margin calculations
readonly property bool barOnThisMonitor: BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.backgroundOpacity > 0
readonly property real barMargin: !Settings.data.bar.floating && barOnThisMonitor ? Style.barHeight : 0
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell-corner"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
margins {
// When bar is floating, corners should be at screen edges (no margins)
// When bar is not floating, respect bar margins as before
top: Settings.data.bar.position === "top" ? barMargin : 0
bottom: Settings.data.bar.position === "bottom" ? barMargin : 0
left: Settings.data.bar.position === "left" ? barMargin : 0
right: Settings.data.bar.position === "right" ? barMargin : 0
}
mask: Region {}
// Reusable corner canvas component
component CornerCanvas: Canvas {
id: corner
required property real arcCenterX
required property real arcCenterY
width: root.cornerSize
height: root.cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
// Fill the entire area with the corner color
ctx.fillStyle = root.cornerColor
ctx.fillRect(0, 0, width, height)
// Cut out the rounded corner using destination-out
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(arcCenterX, arcCenterY, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
}
// Consolidated repaint handler for all corners
property var corners: [topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner]
onCornerColorChanged: {
corners.forEach(corner => {
if (corner.available)
corner.requestPaint()
})
}
onCornerRadiusChanged: {
corners.forEach(corner => {
if (corner.available)
corner.requestPaint()
})
}
// Top-left concave corner
CornerCanvas {
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
arcCenterX: width
arcCenterY: height
}
// Top-right concave corner
CornerCanvas {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
arcCenterX: 0
arcCenterY: height
}
// Bottom-left concave corner
CornerCanvas {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
arcCenterX: width
arcCenterY: 0
}
// Bottom-right concave corner
CornerCanvas {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
arcCenterX: 0
arcCenterY: 0
}
}
}
}

View File

@@ -18,7 +18,6 @@ NPanel {
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
panelKeyboardFocus: true
// Connections to update local volumes when AudioService changes
Connections {

View File

@@ -10,60 +10,92 @@ import qs.Widgets
import qs.Modules.Notification
import qs.Modules.Bar.Extras
Variants {
model: Quickshell.screens
// Bar Component
Item {
id: root
delegate: Loader {
id: root
// This property will be set by NFullScreenWindow
property ShellScreen screen: null
required property ShellScreen modelData
// Expose bar region for click-through mask
readonly property var barRegion: barContentLoader.item?.children[0] || null
active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
// Bar positioning properties
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
sourceComponent: PanelWindow {
screen: modelData || null
// Fill the parent (the Loader)
anchors.fill: parent
WlrLayershell.namespace: "noctalia-bar"
// Register bar when screen becomes available
onScreenChanged: {
if (screen && screen.name) {
Logger.d("Bar", "Bar screen set to:", screen.name)
Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating)
Logger.d("Bar", " Margins - H:", barMarginH, "V:", barMarginV)
BarService.registerBar(screen.name)
}
}
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Style.barHeight
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Style.barHeight : screen.width
color: Color.transparent
// Wait for screen to be set before loading bar content
Loader {
id: barContentLoader
anchors.fill: parent
active: root.screen !== null && root.screen !== undefined
anchors {
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
}
sourceComponent: Item {
anchors.fill: parent
// Floating bar margins - only apply when floating is enabled
// Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom.
margins {
top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL : 0
bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL : 0
left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
}
// Background fill with shadow
NShapedRectangle {
id: bar
Component.onCompleted: {
if (modelData && modelData.name) {
BarService.registerBar(modelData.name)
}
}
// Position and size the bar based on orientation and floating margins
x: (root.barPosition === "right") ? (parent.width - Style.barHeight - root.barMarginH) : root.barMarginH
y: (root.barPosition === "bottom") ? (parent.height - Style.barHeight - root.barMarginV) : root.barMarginV
width: root.barIsVertical ? Style.barHeight : (parent.width - root.barMarginH * 2)
height: root.barIsVertical ? (parent.height - root.barMarginV * 2) : Style.barHeight
Item {
anchors.fill: parent
clip: true
backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
// Background fill with shadow
Rectangle {
id: bar
// 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
anchors.fill: parent
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
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"
// Floating bar rounded corners
radius: Settings.data.bar.floating ? Style.radiusL : 0
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
// Shadow configuration
shadowEnabled: true
shadowBlur: 0.5
// Fade shadow progressively when a panel is attached to the bar to avoid visual disconnection
// shadowOpacity: {
// if (PanelService.openedPanel && PanelService.openedPanel.attachedToBar) {
// // Fade shadow out as panel opens (animationProgress goes from 0 to 1)
// return 1.0 - PanelService.openedPanel.animationProgress
// }
// return 1.0
// }
Behavior on shadowOpacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
MouseArea {
@@ -73,8 +105,11 @@ Variants {
preventStealing: true
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
// Important to pass the screen here so we get the right widget for the actual bar that was clicked.
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter", screen.name))
// Look up for any ControlCenter button on this bar
var widget = BarService.lookupWidget("ControlCenter", root.screen.name)
// Open the panel near the button if any
PanelService.getPanel("controlCenterPanel", root.screen)?.toggle(widget)
mouse.accepted = true
}
}
@@ -84,168 +119,188 @@ Variants {
anchors.fill: parent
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
}
}
}
}
// For vertical bars
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
// For vertical bars
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
clip: true
// Top section (left widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.marginM
spacing: Style.marginS
// Top section (left widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.marginM
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
required property var modelData
required property int index
// Center section (center widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
// Bottom section (right widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
})
Layout.alignment: Qt.AlignHCenter
}
}
}
// For horizontal bars
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
// Center section (center widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
required property var modelData
required property int index
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
})
Layout.alignment: Qt.AlignHCenter
}
}
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
// Bottom section (right widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
required property var modelData
required property int index
// Right Section
RowLayout {
id: rightSection
objectName: "rightSection"
anchors.right: parent.right
anchors.rightMargin: Style.marginS
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
})
Layout.alignment: Qt.AlignHCenter
}
}
}
}
}
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
barDensity: Settings.data.bar.density
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
// For horizontal bars
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
clip: true
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
required property var modelData
required property int index
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
})
Layout.alignment: Qt.AlignVCenter
}
}
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
required property var modelData
required property int index
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
})
Layout.alignment: Qt.AlignVCenter
}
}
}
// Right Section
RowLayout {
id: rightSection
objectName: "rightSection"
anchors.right: parent.right
anchors.rightMargin: Style.marginS
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
required property var modelData
required property int index
widgetId: modelData.id || ""
barDensity: Settings.data.bar.density
widgetScreen: root.screen
widgetProps: ({
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
})
Layout.alignment: Qt.AlignVCenter
}
}
}

View File

@@ -11,8 +11,7 @@ NPanel {
id: root
preferredWidth: 350 * Style.uiScaleRatio
preferredHeight: 250 * Style.uiScaleRatio
panelKeyboardFocus: true
preferredHeight: 210 * Style.uiScaleRatio
property var optionsModel: []

View File

@@ -13,7 +13,6 @@ NPanel {
preferredWidth: 420 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
panelKeyboardFocus: true
panelContent: Rectangle {
color: Color.transparent

View File

@@ -14,7 +14,8 @@ NPanel {
property ShellScreen screen
readonly property var now: Time.date
panelKeyboardFocus: true
preferredWidth: 500
preferredHeight: 700
// Helper function to calculate ISO week number
function getISOWeekNumber(date) {
@@ -44,7 +45,8 @@ NPanel {
return numWeeks * rowHeight
}
property real contentPreferredHeight: banner.height + calendar.height + weatherLoader.height + Style.marginM * 4 + (Settings.data.location.weatherEnabled && Settings.data.location.showCalendarWeather) * Style.marginM
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2
ColumnLayout {
id: content
@@ -65,20 +67,6 @@ NPanel {
isCurrentMonth = checkIsCurrentMonth()
}
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
context: Qt.WidgetShortcut
enabled: root.opened
}
Connections {
target: Time
function onDateChanged() {
@@ -623,7 +611,7 @@ NPanel {
onClicked: {
const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`
Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes])
PanelService.getPanel("calendarPanel").toggle(null)
root.close()
}
onExited: {

View File

@@ -6,15 +6,17 @@ import qs.Commons
Item {
id: root
property string widgetId: ""
property var widgetProps: ({})
property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : ""
property string section: widgetProps && widgetProps.section || ""
property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0
required property string widgetId
required property var widgetScreen
required property var widgetProps
property string barDensity: "default"
readonly property real scaling: barDensity === "mini" ? 0.8 : (barDensity === "compact" ? 0.9 : 1.0)
// Extract section info from widgetProps
readonly property string section: widgetProps.section || ""
readonly property int sectionIndex: widgetProps.sectionWidgetIndex || 0
// Don't reserve space unless the loaded widget is really visible
implicitWidth: getImplicitSize(loader.item, "implicitWidth")
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
@@ -26,56 +28,54 @@ Item {
Loader {
id: loader
anchors.fill: parent
active: widgetId !== ""
asynchronous: false
sourceComponent: {
if (!active) {
return null
}
return BarWidgetRegistry.getWidget(widgetId)
}
sourceComponent: BarWidgetRegistry.getWidget(widgetId)
onLoaded: {
if (item && widgetProps) {
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
}
// Explicitly set scaling property
if (item.hasOwnProperty("scaling")) {
item.scaling = Qt.binding(function () {
return root.scaling
})
if (!item)
return
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name)
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
}
// Set screen property
if (item.hasOwnProperty("screen")) {
item.screen = widgetScreen
}
// Set scaling property
if (item.hasOwnProperty("scaling")) {
item.scaling = Qt.binding(function () {
return root.scaling
})
}
// Register this widget instance with BarService
if (screenName && section) {
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
}
BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item)
// Call custom onLoaded if it exists
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
//Logger.i("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
// Unregister when destroyed
if (screenName && section) {
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
if (widgetScreen && section) {
BarService.unregisterWidget(widgetScreen.name, section, widgetId, sectionIndex)
}
// Explicitly clear references
widgetProps = null
}
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
Component.onCompleted: {
if (!BarWidgetRegistry.hasWidget(widgetId)) {
Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId)
}
}

View File

@@ -12,7 +12,6 @@ NPanel {
preferredWidth: 420 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio
panelKeyboardFocus: true
property string passwordSsid: ""
property string passwordInput: ""

View File

@@ -94,7 +94,7 @@ Item {
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery)
onClicked: PanelService.getPanel("batteryPanel")?.toggle(this)
onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this)
tooltipText: {
let lines = []
if (testMode) {

View File

@@ -54,8 +54,8 @@ Item {
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this)
tooltipText: {
if (pill.text !== "") {
return pill.text

View File

@@ -108,13 +108,13 @@ Item {
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}

View File

@@ -121,7 +121,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
if (!PanelService.getPanel("calendarPanel", screen)?.active) {
TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
}
}
@@ -130,7 +130,7 @@ Rectangle {
}
onClicked: {
TooltipService.hide()
PanelService.getPanel("calendarPanel")?.toggle(this)
PanelService.getPanel("calendarPanel", screen)?.toggle(this)
}
}
}

View File

@@ -44,8 +44,8 @@ NIconButton {
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mHover
colorBorder: Color.transparent
colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
onClicked: PanelService.getPanel("controlCenterPanel", screen)?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle()
IconImage {
id: customOrDistroLogo

View File

@@ -184,7 +184,7 @@ Item {
Logger.i("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
settingsPanel.open()
}

View File

@@ -105,7 +105,7 @@ Item {
}
}
onClicked: {
PanelService.getPanel("audioPanel")?.toggle(this)
PanelService.getPanel("audioPanel", screen)?.toggle(this)
}
onRightClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)

View File

@@ -43,7 +43,7 @@ NIconButton {
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}

View File

@@ -56,7 +56,7 @@ NIconButton {
colorBorderHover: Color.transparent
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel")
var panel = PanelService.getPanel("notificationHistoryPanel", screen)
panel?.toggle(this)
}

View File

@@ -20,5 +20,5 @@ NIconButton {
colorFg: Color.mError
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle()
}

View File

@@ -279,7 +279,6 @@ Rectangle {
function open() {
visible = true
PanelService.willOpenPanel(trayPanel)
}
function close() {

View File

@@ -90,7 +90,7 @@ Item {
}
}
onClicked: {
PanelService.getPanel("audioPanel")?.toggle(this)
PanelService.getPanel("audioPanel", screen)?.toggle(this)
}
onRightClicked: {
AudioService.setOutputMuted(!AudioService.muted)

View File

@@ -20,5 +20,5 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle(this)
}

View File

@@ -76,8 +76,8 @@ Item {
autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this)
tooltipText: {
if (pill.text !== "") {
return pill.text

View File

@@ -60,8 +60,9 @@ NBox {
icon: "settings"
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.open()
var panel = PanelService.getPanel("settingsPanel", screen)
panel.requestedTab = SettingsPanel.Tab.General
panel.open()
}
}
@@ -69,8 +70,8 @@ NBox {
icon: "power"
tooltipText: I18n.tr("tooltips.session-menu")
onClicked: {
sessionMenuPanel.open()
controlCenterPanel.close()
PanelService.getPanel("sessionMenuPanel", screen)?.open()
PanelService.getPanel("controlCenterPanel", screen)?.close()
}
}
@@ -78,7 +79,7 @@ NBox {
icon: "close"
tooltipText: I18n.tr("tooltips.close")
onClicked: {
controlCenterPanel.close()
PanelService.getPanel("controlCenterPanel", screen)?.close()
}
}
}

View File

@@ -28,10 +28,13 @@ RowLayout {
Repeater {
model: Settings.data.controlCenter.shortcuts.left
delegate: ControlCenterWidgetLoader {
required property var modelData
required property int index
Layout.fillWidth: false
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetScreen: root.screen
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "quickSettings",
"sectionWidgetIndex": index,
@@ -63,10 +66,13 @@ RowLayout {
Repeater {
model: Settings.data.controlCenter.shortcuts.right
delegate: ControlCenterWidgetLoader {
required property var modelData
required property int index
Layout.fillWidth: false
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetScreen: root.screen
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "quickSettings",
"sectionWidgetIndex": index,

View File

@@ -10,7 +10,6 @@ import qs.Widgets
NPanel {
id: root
panelKeyboardFocus: true
preferredWidth: Math.round(460 * Style.uiScaleRatio)
preferredHeight: {
var height = 0

View File

@@ -6,9 +6,10 @@ import qs.Commons
Item {
id: root
property string widgetId: ""
property var widgetProps: ({})
property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : ""
required property string widgetId
required property var widgetScreen
required property var widgetProps
property string section: widgetProps && widgetProps.section || ""
property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0
@@ -23,30 +24,29 @@ Item {
Loader {
id: loader
anchors.fill: parent
active: widgetId !== ""
asynchronous: false
sourceComponent: {
if (!active) {
return null
}
return ControlCenterWidgetRegistry.getWidget(widgetId)
}
sourceComponent: ControlCenterWidgetRegistry.getWidget(widgetId)
onLoaded: {
if (item && widgetProps) {
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
if (!item)
return
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
}
// Set screen property
if (item.hasOwnProperty("screen")) {
item.screen = widgetScreen
}
// Call custom onLoaded if it exists
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
//Logger.i("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
@@ -56,8 +56,8 @@ Item {
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) {
Component.onCompleted: {
if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) {
Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId)
}
}

View File

@@ -9,5 +9,7 @@ NIconButtonHot {
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
tooltipText: I18n.tr("quickSettings.bluetooth.tooltip.action")
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onClicked: {
PanelService.getPanel("bluetoothPanel", screen)?.toggle(this)
}
}

View File

@@ -25,7 +25,7 @@ NIconButtonHot {
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}

View File

@@ -10,6 +10,6 @@ NIconButtonHot {
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
hot: Settings.data.notifications.doNotDisturb
tooltipText: I18n.tr("quickSettings.notifications.tooltip.action")
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(this)
onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this)
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}

View File

@@ -14,7 +14,7 @@ NIconButtonHot {
enabled: hasPP
icon: PowerProfileService.getIcon()
hot: !PowerProfileService.isDefault()
tooltipText: hasPP ? I18n.tr("quickSettings.powerProfile.tooltip.action") : I18n.tr("quickSettings.powerProfile.tooltip.disabled")
tooltipText: I18n.tr("quickSettings.powerProfile.tooltip.action")
onClicked: {
PowerProfileService.cycleProfile()
}

View File

@@ -14,7 +14,7 @@ NIconButtonHot {
onClicked: {
ScreenRecorderService.toggleRecording()
if (!ScreenRecorderService.isRecording) {
var panel = PanelService.getPanel("controlCenterPanel")
var panel = PanelService.getPanel("controlCenterPanel", screen)
panel?.close()
}
}

View File

@@ -10,6 +10,6 @@ NIconButtonHot {
enabled: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: I18n.tr("quickSettings.wallpaperSelector.tooltip.action")
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle()
onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle()
onRightClicked: WallpaperService.setRandomWallpaper()
}

View File

@@ -29,5 +29,5 @@ NIconButtonHot {
}
tooltipText: I18n.tr("quickSettings.wifi.tooltip.action")
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this)
}

View File

@@ -16,8 +16,8 @@ NPanel {
preferredWidthRatio: 0.3
preferredHeightRatio: 0.5
panelKeyboardFocus: true
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity)
panelKeyboardFocus: true // Needs Exclusive focus for text input
// Positioning
readonly property string launcherPosition: Settings.data.appLauncher.position
@@ -40,6 +40,51 @@ 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
function onTabPressed() {
selectNextWrapped()
}
function onShiftTabPressed() {
selectPreviousWrapped()
}
function onUpPressed() {
selectPreviousWrapped()
}
function onDownPressed() {
selectNextWrapped()
}
function onReturnPressed() {
activate()
}
function onHomePressed() {
selectFirst()
}
function onEndPressed() {
selectLast()
}
function onPageUpPressed() {
selectPreviousPage()
}
function onPageDownPressed() {
selectNextPage()
}
function onCtrlJPressed() {
selectNextWrapped()
}
function onCtrlKPressed() {
selectPreviousWrapped()
}
// Public API for plugins
function setSearchText(text) {
searchText = text
@@ -151,6 +196,54 @@ NPanel {
}
}
// Navigation functions
function selectNextWrapped() {
if (results.length > 0) {
selectedIndex = (selectedIndex + 1) % results.length
}
}
function selectPreviousWrapped() {
if (results.length > 0) {
selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (results.length > 0) {
selectedIndex = results.length - 1
} else {
selectedIndex = 0
}
}
function selectNextPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height
selectedIndex = Math.min(selectedIndex + page, results.length - 1)
}
}
function selectPreviousPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height
selectedIndex = Math.max(selectedIndex - page, 0)
}
}
function activate() {
if (results.length > 0 && results[selectedIndex]) {
const item = results[selectedIndex]
if (item.onActivate) {
item.onActivate()
}
}
}
// UI
panelContent: Rectangle {
id: ui
@@ -198,6 +291,19 @@ NPanel {
}
}
// Focus management
Connections {
target: root
function onOpened() {
// Delay focus to ensure window has keyboard focus
Qt.callLater(() => {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
}
})
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
@@ -205,102 +311,6 @@ NPanel {
}
}
// ---------------------
// Navigation
function selectNextWrapped() {
if (results.length > 0) {
selectedIndex = (selectedIndex + 1) % results.length
}
}
function selectPreviousWrapped() {
if (results.length > 0) {
selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (results.length > 0) {
selectedIndex = results.length - 1
} else {
selectedIndex = 0
}
}
function selectNextPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
selectedIndex = Math.min(selectedIndex + page, results.length - 1)
}
}
function selectPreviousPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
selectedIndex = Math.max(selectedIndex - page, 0)
}
}
function activate() {
if (results.length > 0 && results[selectedIndex]) {
const item = results[selectedIndex]
if (item.onActivate) {
item.onActivate()
}
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPreviousWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNextWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Tab"
onActivated: ui.selectNextWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Shift+Tab"
onActivated: ui.selectPreviousWrapped()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgDown" // or "PageDown"
onActivated: ui.selectNextPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgUp" // or "PageUp"
onActivated: ui.selectPreviousPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
@@ -319,36 +329,8 @@ NPanel {
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
// Override the TextField's default Home/End behavior
searchInput.inputItem.Keys.priority = Keys.BeforeItem
searchInput.inputItem.Keys.onPressed.connect(function (event) {
// Intercept Home, End, and Numpad Enter BEFORE the TextField handles them
if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
return
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
return
} else if (event.key === Qt.Key_Enter) {
ui.activate()
event.accepted = true
return
}
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
ui.selectNextWrapped()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
ui.selectPreviousWrapped()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
})
}
}
}
@@ -588,7 +570,7 @@ NPanel {
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
selectedIndex = index
ui.activate()
root.activate()
mouse.accepted = true
}
}

View File

@@ -12,9 +12,8 @@ import qs.Widgets
NPanel {
id: root
preferredWidth: 380
preferredHeight: 480
panelKeyboardFocus: true
preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 480 * Style.uiScaleRatio
onOpened: function () {
NotificationService.updateLastSeenTs()

View File

@@ -15,9 +15,9 @@ NPanel {
preferredWidth: 400 * Style.uiScaleRatio
preferredHeight: 340 * Style.uiScaleRatio
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
@@ -148,6 +148,52 @@ NPanel {
}
}
// Override keyboard handlers from NPanel
function onEscapePressed() {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
close()
}
}
function onTabPressed() {
selectNextWrapped()
}
function onShiftTabPressed() {
selectPreviousWrapped()
}
function onUpPressed() {
selectPreviousWrapped()
}
function onDownPressed() {
selectNextWrapped()
}
function onReturnPressed() {
activate()
}
function onHomePressed() {
selectFirst()
}
function onEndPressed() {
selectLast()
}
function onCtrlJPressed() {
selectNextWrapped()
}
function onCtrlKPressed() {
selectPreviousWrapped()
}
// Countdown timer
Timer {
id: countdownTimer
@@ -165,81 +211,6 @@ NPanel {
id: ui
color: Color.transparent
// Keyboard shortcuts
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPreviousWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNextWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: ui.selectPreviousWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Down"
onActivated: ui.selectNextWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: ui.selectPreviousWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Tab"
onActivated: ui.selectNextWrapped()
enabled: root.opened
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened
}
Shortcut {
sequence: "Return"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Enter"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
context: Qt.WidgetShortcut
enabled: root.opened
}
// Navigation functions
function selectFirst() {
root.selectFirst()

View File

@@ -16,9 +16,6 @@ NPanel {
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
draggable: !PanelService.hasOpenedPopup
// Tabs enumeration, order is NOT relevant
enum Tab {
@@ -287,6 +284,39 @@ NPanel {
}
}
// Override keyboard handlers from NPanel
function onTabPressed() {
selectNextTab()
}
function onShiftTabPressed() {
selectPreviousTab()
}
function onUpPressed() {
scrollUp()
}
function onDownPressed() {
scrollDown()
}
function onPageUpPressed() {
scrollPageUp()
}
function onPageDownPressed() {
scrollPageDown()
}
function onCtrlJPressed() {
scrollDown()
}
function onCtrlKPressed() {
scrollUp()
}
panelContent: Rectangle {
color: Color.transparent
@@ -296,62 +326,6 @@ NPanel {
anchors.margins: Style.marginL
spacing: 0
// Keyboard shortcuts container
Item {
Layout.preferredWidth: 0
Layout.preferredHeight: 0
// Scrolling via keyboard
Shortcut {
sequence: "Down"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: root.scrollDown()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+K"
onActivated: root.scrollUp()
enabled: root.opened
}
Shortcut {
sequence: "PgDown"
onActivated: root.scrollPageDown()
enabled: root.opened
}
Shortcut {
sequence: "PgUp"
onActivated: root.scrollPageUp()
enabled: root.opened
}
// Changing tab via keyboard
Shortcut {
sequence: "Tab"
onActivated: root.selectNextTab()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: root.selectPreviousTab()
enabled: root.opened
}
}
// Main content area
RowLayout {
Layout.fillWidth: true

View File

@@ -25,7 +25,7 @@ ColumnLayout {
// Handler for drag start - disables panel background clicks
function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel")
var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick()
}
@@ -33,7 +33,7 @@ ColumnLayout {
// Handler for drag end - re-enables panel background clicks
function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel")
var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick()
}
@@ -102,6 +102,14 @@ ColumnLayout {
onToggled: checked => Settings.data.bar.floating = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("settings.bar.appearance.outer-corners.label")
description: I18n.tr("settings.bar.appearance.outer-corners.description")
checked: Settings.data.bar.outerCorners
onToggled: checked => Settings.data.bar.outerCorners = checked
}
// Floating bar options - only show when floating is enabled
ColumnLayout {
visible: Settings.data.bar.floating

View File

@@ -40,7 +40,7 @@ ColumnLayout {
// Handler for drag start - disables panel background clicks
function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel")
var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick()
}
@@ -48,7 +48,7 @@ ColumnLayout {
// Handler for drag end - re-enables panel background clicks
function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel")
var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick()
}

View File

@@ -22,6 +22,9 @@ ColumnLayout {
model: [{
"key": "center",
"name": I18n.tr("options.launcher.position.center")
}, {
"key": "top_center",
"name": I18n.tr("options.launcher.position.top_center")
}, {
"key": "top_left",
"name": I18n.tr("options.launcher.position.top_left")
@@ -37,9 +40,6 @@ ColumnLayout {
}, {
"key": "bottom_center",
"name": I18n.tr("options.launcher.position.bottom_center")
}, {
"key": "top_center",
"name": I18n.tr("options.launcher.position.top_center")
}]
currentKey: Settings.data.appLauncher.position
onSelected: function (key) {

View File

@@ -26,6 +26,13 @@ ColumnLayout {
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked
}
NToggle {
label: I18n.tr("settings.user-interface.dim-desktop.label")
description: I18n.tr("settings.user-interface.dim-desktop.description")
checked: Settings.data.general.dimDesktop
onToggled: checked => Settings.data.general.dimDesktop = checked
}
NToggle {
label: I18n.tr("settings.user-interface.panels-attached-to-bar.label")
description: I18n.tr("settings.user-interface.panels-attached-to-bar.description")

View File

@@ -14,17 +14,17 @@ NPanel {
preferredHeight: 600 * Style.uiScaleRatio
preferredWidthRatio: 0.4
preferredHeightRatio: 0.6
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Prevent closing during setup
backgroundClickEnabled: false
draggable: false
property int currentStep: 0
property int totalSteps: 5
// Override Escape handler to prevent closing the setup wizard
function onEscapePressed() {// Do nothing - prevent ESC from closing the setup wizard
}
// Setup wizard data
property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory
property string selectedWallpaper: ""
@@ -42,17 +42,6 @@ NPanel {
anchors.margins: Style.marginXL
spacing: Style.marginL
// Override ESC key to prevent closing during setup
Shortcut {
sequences: ["Escape"]
enabled: root.active
onActivated: {
// Do nothing - prevent ESC from closing the setup wizard
}
context: Qt.WindowShortcut
}
// Step content - takes most of the space
Item {
Layout.fillWidth: true

View File

@@ -12,15 +12,87 @@ import "../../Helpers/FuzzySort.js" as FuzzySort
NPanel {
id: root
preferredWidth: 640 * Style.uiScaleRatio
preferredHeight: 480 * Style.uiScaleRatio
preferredWidthRatio: 0.4
preferredWidth: 800 * Style.uiScaleRatio
preferredHeight: 600 * Style.uiScaleRatio
preferredWidthRatio: 0.5
preferredHeightRatio: 0.52
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
draggable: !PanelService.hasOpenedPopup
// Positioning - Use launcher position. This saves a setting...
readonly property string launcherPosition: Settings.data.appLauncher.position
panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center")
panelAnchorVerticalCenter: launcherPosition === "center"
panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left")
panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right")
panelAnchorBottom: launcherPosition.startsWith("bottom_")
panelAnchorTop: launcherPosition.startsWith("top_")
// panelAnchorHorizontalCenter: true
// panelAnchorVerticalCenter: true
panelKeyboardFocus: true // Needs Exclusive focus for text input (search)
// Store direct reference to content for instant access
property var contentItem: null
// Override keyboard handlers to enable grid navigation
function onDownPressed() {
if (!contentItem)
return
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex)
if (view?.gridView) {
if (!view.gridView.activeFocus) {
view.gridView.forceActiveFocus()
if (view.gridView.currentIndex < 0) {
view.gridView.currentIndex = 0
}
} else {
view.gridView.moveCurrentIndexDown()
}
}
}
function onUpPressed() {
if (!contentItem)
return
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex)
if (view?.gridView?.activeFocus) {
view.gridView.moveCurrentIndexUp()
}
}
function onLeftPressed() {
if (!contentItem)
return
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex)
if (view?.gridView?.activeFocus) {
view.gridView.moveCurrentIndexLeft()
}
}
function onRightPressed() {
if (!contentItem)
return
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex)
if (view?.gridView?.activeFocus) {
view.gridView.moveCurrentIndexRight()
}
}
function onReturnPressed() {
if (!contentItem)
return
let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex)
if (view?.gridView?.activeFocus) {
let gridView = view.gridView
if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.length) {
let path = gridView.model[gridView.currentIndex]
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(path, undefined)
} else {
WallpaperService.changeWallpaper(path, view.targetScreen.name)
}
}
}
}
panelContent: Rectangle {
id: wallpaperPanel
@@ -37,9 +109,31 @@ NPanel {
}
property var currentScreen: Quickshell.screens[currentScreenIndex]
property string filterText: ""
property alias screenRepeater: screenRepeater
Component.onCompleted: {
root.contentItem = wallpaperPanel
}
color: Color.transparent
// Focus management
Connections {
target: root
function onOpened() {
// Ensure contentItem is set
if (!root.contentItem) {
root.contentItem = wallpaperPanel
}
// Give initial focus to search input
Qt.callLater(() => {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
}
})
}
}
// Debounce timer for search
Timer {
id: searchDebounceTimer
@@ -85,7 +179,7 @@ NPanel {
tooltipText: I18n.tr("settings.wallpaper.settings.section.label")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper
settingsPanel.open()
}
@@ -324,7 +418,7 @@ NPanel {
model: filteredWallpapers
property int columns: 4
property int columns: 5
property int itemSize: cellWidth
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)

View File

@@ -62,7 +62,7 @@ Singleton {
} else {
BatteryService.initialSetter = true
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup"))
PanelService.getPanel("batteryPanel")?.toggle(this)
PanelService.getPanel("batteryPanel", screen)?.toggle(this)
uninstallerProcess.running = true
}
}
@@ -123,7 +123,7 @@ Singleton {
Settings.data.battery.chargingMode = BatteryService.chargingMode
} else if (exitCode === 2) {
ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup"))
PanelService.getPanel("batteryPanel")?.toggle(this)
PanelService.getPanel("batteryPanel", screen)?.toggle(this)
BatteryService.runInstaller()
} else {
ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed"))

View File

@@ -8,7 +8,15 @@ import qs.Commons
Singleton {
id: root
property bool shouldRun: BarService.hasAudioVisualizer || (PanelService.getPanel("controlCenterPanel") === PanelService.openedPanel) || PanelService.lockScreen.active
/**
* Cava runs if:
* - Bar has an audio visualizer
* - LockScreen is opened
* - A control center is open
*/
property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel"))
property var values: Array(barsCount).fill(0)
property int barsCount: 48
property var config: ({

View File

@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
@@ -27,7 +28,10 @@ Item {
IpcHandler {
target: "settings"
function toggle() {
settingsPanel.toggle()
root.withTargetScreen(screen => {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.toggle()
})
}
}
@@ -35,7 +39,10 @@ Item {
target: "notifications"
function toggleHistory() {
// Will attempt to open the panel next to the bar button if any.
notificationHistoryPanel.toggle(null, "NotificationHistory")
root.withTargetScreen(screen => {
var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen)
notificationHistoryPanel.toggle(null, "NotificationHistory")
})
}
function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
@@ -63,15 +70,24 @@ Item {
IpcHandler {
target: "launcher"
function toggle() {
launcherPanel.toggle()
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.toggle()
})
}
function clipboard() {
launcherPanel.setSearchText(">clip ")
launcherPanel.toggle()
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.setSearchText(">clip ")
launcherPanel.toggle()
})
}
function calculator() {
launcherPanel.setSearchText(">calc ")
launcherPanel.toggle()
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.setSearchText(">calc ")
launcherPanel.toggle()
})
}
}
@@ -158,7 +174,10 @@ Item {
IpcHandler {
target: "sessionMenu"
function toggle() {
sessionMenuPanel.toggle()
root.withTargetScreen(screen => {
var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen)
sessionMenuPanel.toggle()
})
}
function lockAndSuspend() {
@@ -170,7 +189,10 @@ Item {
target: "controlCenter"
function toggle() {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel.toggle(null, "ControlCenter")
root.withTargetScreen(screen => {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen)
controlCenterPanel.toggle(null, "ControlCenter")
})
}
}
@@ -179,7 +201,10 @@ Item {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
wallpaperPanel.toggle()
root.withTargetScreen(screen => {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen)
wallpaperPanel.toggle()
})
}
}
@@ -228,6 +253,7 @@ Item {
}
}
}
IpcHandler {
target: "powerProfile"
function cycle() {
@@ -248,6 +274,7 @@ Item {
}
}
}
IpcHandler {
target: "media"
function playPause() {
@@ -292,4 +319,83 @@ Item {
MediaService.seekByRatio(positionVal)
}
}
// Queue an IPC panel operation - will execute when screen is detected
function withTargetScreen(callback) {
if (pendingCallback) {
Logger.w("IPC", "Another IPC call is pending, ignoring new call")
return
}
// Single monitor setup can execute immediately
if (Quickshell.screens.length === 1) {
pendingCallback(Quickshell.screens[0])
} else {
// Multi-monitors setup needs to start async detection
detectedScreen = null
pendingCallback = callback
screenDetectorLoader.active = true
}
}
/**
* For IPC calls on multi-monitors setup that will open panels on screen,
* we need to open a QS PanelWindow and wait for it's "screen" property to stabilize.
*/
property ShellScreen detectedScreen: null
property var pendingCallback: null
Timer {
id: screenDetectorDebounce
running: false
interval: 20
onTriggered: {
Logger.d("IPC", "Screen debounced to:", detectedScreen?.name || "null")
// Execute pending callback if any
if (pendingCallback) {
// Verify we have a NFullScreenWindow for this screen
var monitors = Settings.data.bar.monitors || []
if (!(monitors.length === 0 || monitors.includes(detectedScreen.name))) {
// Fall back to first enabled screen as we can NOT show a panel on a screen without a Bar/NFullScreenWindow
if (monitors.length === 0 && Quickshell.screens.length > 0) {
detectedScreen = Quickshell.screens[0]
} else {
for (var i = 0; i < Quickshell.screens.length; i++) {
if (monitors.includes(Quickshell.screens[i].name)) {
detectedScreen = Quickshell.screens[i]
break
}
}
}
}
Logger.d("IPC", "Executing pending IPC callback on screen:", detectedScreen.name)
pendingCallback(detectedScreen)
pendingCallback = null
}
// Clean up
screenDetectorLoader.active = false
}
}
// Invisible dummy PanelWindow to detect which screen should receive IPC calls
Loader {
id: screenDetectorLoader
active: false
sourceComponent: PanelWindow {
implicitWidth: 0
implicitHeight: 0
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
mask: Region {}
onScreenChanged: {
detectedScreen = screen
screenDetectorDebounce.restart()
}
}
}
}

View File

@@ -304,7 +304,7 @@ Singleton {
repeat: true
running: true
onTriggered: {
Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused)
//Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused)
if (autoSwitchingPaused)
return
// Only update if we don't have a playing player or if current player is paused

View File

@@ -14,6 +14,7 @@ Singleton {
property var registeredPanels: ({})
property var openedPanel: null
signal willOpen
signal didClose
// Currently opened popups, can have more than one.
// ex: when opening an NIconPicker from a widget setting.
@@ -21,15 +22,53 @@ Singleton {
property bool hasOpenedPopup: false
signal popupChanged
// Register this panel
function registerPanel(panel) {
registeredPanels[panel.objectName] = panel
Logger.d("PanelService", "Registered:", panel.objectName)
// Registered panel loaders (before they're loaded)
property var registeredPanelLoaders: ({})
// Register a panel loader (called before panel is loaded)
function registerPanelLoader(panelLoader, objectName) {
registeredPanelLoaders[objectName] = panelLoader
Logger.d("PanelService", "Registered panel loader:", objectName)
}
// Returns a panel
function getPanel(name) {
return registeredPanels[name] || null
// Register this panel (called after panel is loaded)
function registerPanel(panel) {
registeredPanels[panel.objectName] = panel
Logger.i("PanelService", "Registered panel:", panel.objectName)
}
// Returns a panel (loads it on-demand if not yet loaded)
function getPanel(name, screen) {
if (!screen) {
Logger.w("PanelService", "missing screen for getPanel:", name)
Logger.callStack()
// If no screen specified, return the first matching panel
for (var key in registeredPanels) {
if (key.startsWith(name + "-")) {
return registeredPanels[key]
}
}
return null
}
var panelKey = `${name}-${screen.name}`
// Check if panel is already loaded
if (registeredPanels[panelKey]) {
return registeredPanels[panelKey]
}
// Panel not loaded yet - try to load it via the loader
if (registeredPanelLoaders[panelKey]) {
Logger.d("PanelService", "Loading panel on-demand:", panelKey)
registeredPanelLoaders[panelKey].ensureLoaded()
// After ensureLoaded(), the panel should register itself via registerPanel()
// Return it if it registered synchronously
return registeredPanels[panelKey] || null
}
Logger.w("PanelService", "Panel not found:", panelKey)
return null
}
// Check if a panel exists
@@ -52,6 +91,9 @@ Singleton {
if (openedPanel && openedPanel === panel) {
openedPanel = null
}
// emit signal
didClose()
}
// Popups

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 NFullScreenWindow.
*/
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)
}
}

View File

@@ -0,0 +1,608 @@
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()
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Effects
import qs.Commons
Item {
@@ -24,8 +25,17 @@ Item {
property string bottomLeftInvertedDirection: "horizontal" // default: curves left
property string bottomRightInvertedDirection: "horizontal" // default: curves right
// Background color
property color backgroundColor: "white"
// Background color and borders
property color backgroundColor: Color.mPrimary
property color borderColor: Color.mOutline
property int borderWidth: Style.borderS
// Shadow properties
property bool shadowEnabled: true
property real shadowOpacity: 1.0 // 0.0 to 1.0
property real shadowBlur: 0.9
property real shadowHorizontalOffset: 3
property real shadowVerticalOffset: 3
// Check if any corner is inverted
readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted
@@ -36,144 +46,174 @@ Item {
readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0)
readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0)
// Simple rectangle for non-inverted corners (better performance)
Rectangle {
id: simpleBackground
// Background layer: shape with shadow effects (layer.enabled)
Item {
id: shadowLayer
anchors.fill: parent
color: root.backgroundColor
radius: topLeftRadius // Use topLeftRadius as default
border.width: Style.borderS
border.color: Color.mOutline
visible: !root.hasInvertedCorners
z: 0
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
// Apply shadow effect to this layer only
layer.enabled: root.shadowEnabled
layer.smooth: true
// layer.textureSize: Qt.size(width * Screen.devicePixelRatio, height * Screen.devicePixelRatio)
layer.effect: MultiEffect {
shadowEnabled: root.shadowEnabled
shadowOpacity: root.shadowOpacity
shadowColor: "#000000"
shadowHorizontalOffset: root.shadowHorizontalOffset
shadowVerticalOffset: root.shadowVerticalOffset
blur: root.shadowBlur
blurMax: 32
}
// Background with custom corners (for inverted corners)
Canvas {
id: background
anchors.fill: parent
anchors.topMargin: -root.topPadding
anchors.bottomMargin: -root.bottomPadding
anchors.leftMargin: -root.leftPadding
anchors.rightMargin: -root.rightPadding
visible: root.hasInvertedCorners
// Simple rectangle for non-inverted corners (better performance)
Rectangle {
id: simpleBackground
anchors.fill: parent
color: root.backgroundColor
radius: topLeftRadius // Use topLeftRadius as default
border.width: borderWidth
border.color: borderColor
visible: !root.hasInvertedCorners
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
// Background with custom corners (for inverted corners)
Canvas {
id: background
anchors.fill: parent
anchors.topMargin: -root.topPadding
anchors.bottomMargin: -root.bottomPadding
anchors.leftMargin: -root.leftPadding
anchors.rightMargin: -root.rightPadding
visible: root.hasInvertedCorners
// Adjust coordinates to account for inverted corner padding
var x = root.leftPadding
var y = root.topPadding
var w = width - root.leftPadding - root.rightPadding
var h = height - root.topPadding - root.bottomPadding
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
ctx.fillStyle = root.backgroundColor
ctx.beginPath()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
// Start from top left
if (topLeftInverted) {
if (topLeftInvertedDirection === "vertical") {
ctx.moveTo(x, y)
// Adjust coordinates to account for inverted corner padding
var x = root.leftPadding
var y = root.topPadding
var w = width - root.leftPadding - root.rightPadding
var h = height - root.topPadding - root.bottomPadding
ctx.fillStyle = root.backgroundColor
ctx.beginPath()
// Start from top left
if (topLeftInverted) {
if (topLeftInvertedDirection === "vertical") {
ctx.moveTo(x, y)
} else {
ctx.moveTo(x + topLeftRadius, y)
}
} else {
ctx.moveTo(x + topLeftRadius, y)
}
} else {
ctx.moveTo(x + topLeftRadius, y)
}
// Top edge and top right corner
if (topRightInverted) {
if (topRightInvertedDirection === "horizontal") {
// Curves to the right
ctx.lineTo(x + w, y)
ctx.lineTo(x + w + topRightRadius, y)
ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius)
// Top edge and top right corner
if (topRightInverted) {
if (topRightInvertedDirection === "horizontal") {
// Curves to the right
ctx.lineTo(x + w, y)
ctx.lineTo(x + w + topRightRadius, y)
ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius)
} else {
// Curves upward
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y - topRightRadius)
ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y)
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y + topRightRadius)
}
} else {
// Curves upward
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y - topRightRadius)
ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y)
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y + topRightRadius)
ctx.lineTo(x + w - topRightRadius, y)
ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
}
} else {
ctx.lineTo(x + w - topRightRadius, y)
ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
}
// Right edge and bottom right corner
if (bottomRightInverted) {
if (bottomRightInvertedDirection === "horizontal") {
// Curves to the right
// Right edge and bottom right corner
if (bottomRightInverted) {
if (bottomRightInvertedDirection === "horizontal") {
// Curves to the right
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h)
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w - bottomRightRadius, y + h)
} else {
// Curves downward
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w, y + h + bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h)
}
} else {
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h)
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w - bottomRightRadius, y + h)
} else {
// Curves downward
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w, y + h + bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h)
ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius)
}
} else {
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius)
}
// Bottom edge and bottom left corner
if (bottomLeftInverted) {
if (bottomLeftInvertedDirection === "horizontal") {
// Curves to the left
// Bottom edge and bottom left corner
if (bottomLeftInverted) {
if (bottomLeftInvertedDirection === "horizontal") {
// Curves to the left
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.lineTo(x - bottomLeftRadius, y + h)
ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius)
} else {
// Curves downward
ctx.lineTo(x, y + h)
ctx.lineTo(x, y + h + bottomLeftRadius)
ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h)
ctx.lineTo(x, y + h)
ctx.lineTo(x, y + h - bottomLeftRadius)
}
} else {
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.lineTo(x - bottomLeftRadius, y + h)
ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius)
} else {
// Curves downward
ctx.lineTo(x, y + h)
ctx.lineTo(x, y + h + bottomLeftRadius)
ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h)
ctx.lineTo(x, y + h)
ctx.lineTo(x, y + h - bottomLeftRadius)
ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius)
}
} else {
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius)
}
// Left edge and back to top left corner
if (topLeftInverted) {
if (topLeftInvertedDirection === "horizontal") {
// Curves to the left
ctx.lineTo(x, y + topLeftRadius)
ctx.quadraticCurveTo(x, y, x - topLeftRadius, y)
ctx.lineTo(x, y)
ctx.lineTo(x + topLeftRadius, y)
// Left edge and back to top left corner
if (topLeftInverted) {
if (topLeftInvertedDirection === "horizontal") {
// Curves to the left
ctx.lineTo(x, y + topLeftRadius)
ctx.quadraticCurveTo(x, y, x - topLeftRadius, y)
ctx.lineTo(x, y)
ctx.lineTo(x + topLeftRadius, y)
} else {
// Curves upward
ctx.lineTo(x, y + topLeftRadius)
ctx.lineTo(x, y)
ctx.lineTo(x, y - topLeftRadius)
ctx.quadraticCurveTo(x, y, x + topLeftRadius, y)
}
} else {
// Curves upward
ctx.lineTo(x, y + topLeftRadius)
ctx.lineTo(x, y)
ctx.lineTo(x, y - topLeftRadius)
ctx.quadraticCurveTo(x, y, x + topLeftRadius, y)
ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
}
} else {
ctx.lineTo(x, y + topLeftRadius)
ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
}
ctx.closePath()
ctx.fill()
ctx.closePath()
ctx.fill()
}
}
}
// Content layer: for child elements (NO layer effects - keeps text sharp)
// Child components can be added here and will render on top without blur
default property alias contentChildren: contentLayer.data
Item {
id: contentLayer
anchors.fill: parent
z: 1
}
// Trigger repaint when properties change
onTopLeftRadiusChanged: background.requestPaint()
onTopRightRadiusChanged: background.requestPaint()

229
shell.qml
View File

@@ -75,6 +75,73 @@ ShellRoot {
}
}
// ------------------------------
// Define panel components (must be at ShellRoot level for NFullScreenWindow access)
Component {
id: launcherComponent
Launcher {}
}
Component {
id: controlCenterComponent
ControlCenterPanel {}
}
Component {
id: calendarComponent
CalendarPanel {}
}
Component {
id: settingsComponent
SettingsPanel {}
}
Component {
id: directWidgetSettingsComponent
DirectWidgetSettingsPanel {}
}
Component {
id: notificationHistoryComponent
NotificationHistoryPanel {}
}
Component {
id: sessionMenuComponent
SessionMenu {}
}
Component {
id: wifiComponent
WiFiPanel {}
}
Component {
id: bluetoothComponent
BluetoothPanel {}
}
Component {
id: audioComponent
AudioPanel {}
}
Component {
id: wallpaperComponent
WallpaperPanel {}
}
Component {
id: batteryComponent
BatteryPanel {}
}
Component {
id: barComp
Bar {}
}
Loader {
active: i18nLoaded && settingsLoaded
@@ -99,8 +166,7 @@ ShellRoot {
Background {}
Overview {}
ScreenCorners {}
Bar {}
Dock {}
Notification {
@@ -121,67 +187,118 @@ ShellRoot {
// IPCService is treated as a service
// but it's actually an Item that needs to exists in the shell.
IPCService {}
}
}
// ------------------------------
// All the NPanels
Launcher {
id: launcherPanel
objectName: "launcherPanel"
// ------------------------------
// NFullScreenWindow for each screen (manages bar + all panels)
// Wrapped in Loader to optimize memory - only loads when screen needs it
Variants {
model: Quickshell.screens
delegate: Item {
required property ShellScreen modelData
property bool shouldBeActive: {
if (!i18nLoaded || !settingsLoaded)
return false
if (!BarService.isVisible)
return false
if (!modelData || !modelData.name)
return false
var monitors = Settings.data.bar.monitors || []
var result = monitors.length === 0 || monitors.includes(modelData.name)
Logger.d("Shell", "NFullScreenWindow Loader for", modelData?.name, "- shouldBeActive:", result, "- monitors:", JSON.stringify(monitors))
return result
}
ControlCenterPanel {
id: controlCenterPanel
objectName: "controlCenterPanel"
property bool windowLoaded: false
Loader {
id: windowLoader
active: parent.shouldBeActive
asynchronous: false
property ShellScreen loaderScreen: modelData
onLoaded: {
// Signal that window is loaded so exclusion zone can be created
parent.windowLoaded = true
}
sourceComponent: NFullScreenWindow {
screen: windowLoader.loaderScreen
// Register all panel components
panelComponents: [{
"id": "launcherPanel",
"component": launcherComponent,
"zIndex": 50
}, {
"id": "controlCenterPanel",
"component": controlCenterComponent,
"zIndex": 50
}, {
"id": "calendarPanel",
"component": calendarComponent,
"zIndex": 50
}, {
"id": "settingsPanel",
"component": settingsComponent,
"zIndex": 50
}, {
"id": "directWidgetSettingsPanel",
"component": directWidgetSettingsComponent,
"zIndex": 50
}, {
"id": "notificationHistoryPanel",
"component": notificationHistoryComponent,
"zIndex": 50
}, {
"id": "sessionMenuPanel",
"component": sessionMenuComponent,
"zIndex": 50
}, {
"id": "wifiPanel",
"component": wifiComponent,
"zIndex": 50
}, {
"id": "bluetoothPanel",
"component": bluetoothComponent,
"zIndex": 50
}, {
"id": "audioPanel",
"component": audioComponent,
"zIndex": 50
}, {
"id": "wallpaperPanel",
"component": wallpaperComponent,
"zIndex": 50
}, {
"id": "batteryPanel",
"component": batteryComponent,
"zIndex": 50
}]
// Bar component
barComponent: barComp
}
}
CalendarPanel {
id: calendarPanel
objectName: "calendarPanel"
}
// BarExclusionZone - created after NFullScreenWindow has fully loaded
// Must also be disabled when bar is disabled (follows shouldBeActive)
Loader {
active: parent.windowLoaded && parent.shouldBeActive
asynchronous: false
SettingsPanel {
id: settingsPanel
objectName: "settingsPanel"
}
sourceComponent: BarExclusionZone {
screen: modelData
}
DirectWidgetSettingsPanel {
id: directWidgetSettingsPanel
objectName: "directWidgetSettingsPanel"
}
NotificationHistoryPanel {
id: notificationHistoryPanel
objectName: "notificationHistoryPanel"
}
SessionMenu {
id: sessionMenuPanel
objectName: "sessionMenuPanel"
}
WiFiPanel {
id: wifiPanel
objectName: "wifiPanel"
}
BluetoothPanel {
id: bluetoothPanel
objectName: "bluetoothPanel"
}
AudioPanel {
id: audioPanel
objectName: "audioPanel"
}
WallpaperPanel {
id: wallpaperPanel
objectName: "wallpaperPanel"
}
BatteryPanel {
id: batteryPanel
objectName: "batteryPanel"
onLoaded: {
Logger.d("Shell", "BarExclusionZone created for", modelData?.name)
}
}
}
}