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

View File

@@ -226,7 +226,11 @@
}, },
"floating": { "floating": {
"label": "Floating bar", "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": { "margins": {
"label": "Margins", "label": "Margins",
@@ -577,6 +581,10 @@
"terminal": { "terminal": {
"label": "Terminal", "label": "Terminal",
"description": "Terminal emulator theming.", "description": "Terminal emulator theming.",
"alacritty": {
"description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed"
},
"kitty": { "kitty": {
"description": "Write {filepath} and reload", "description": "Write {filepath} and reload",
"description-missing": "Requires {app} to be installed" "description-missing": "Requires {app} to be installed"
@@ -851,6 +859,10 @@
"description": "Changes the size of the general user interface, excluding the bar.", "description": "Changes the size of the general user interface, excluding the bar.",
"reset-scaling": "Reset interface scaling" "reset-scaling": "Reset interface scaling"
}, },
"dim-desktop": {
"label": "Dim desktop",
"description": "Dim the desktop when panels or menus are open."
},
"border-radius": { "border-radius": {
"label": "Border radius", "label": "Border radius",
"description": "Controls the corner roundness of windows, buttons, and other elements.", "description": "Controls the corner roundness of windows, buttons, and other elements.",

View File

@@ -226,13 +226,17 @@
}, },
"floating": { "floating": {
"label": "Barra flotante", "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": { "margins": {
"label": "Márgenes", "label": "Márgenes",
"description": "Ajusta los márgenes alrededor de la barra flotante.", "description": "Ajusta los márgenes alrededor de la barra flotante.",
"vertical": "Vertical", "vertical": "Vertical",
"horizontal": "Horizontal" "horizontal": "Horizontal"
},
"outer-corners": {
"description": "Muestra esquinas curvadas hacia afuera en la barra.",
"label": "Esquinas exteriores"
} }
}, },
"widgets": { "widgets": {
@@ -584,6 +588,10 @@
"foot": { "foot": {
"description": "Escribir {filepath} y recargar", "description": "Escribir {filepath} y recargar",
"description-missing": "Requiere que {app} esté instalado" "description-missing": "Requiere que {app} esté instalado"
},
"alacritty": {
"description": "Escribe {filepath} y recarga",
"description-missing": "Requiere que {app} esté instalado/a."
} }
}, },
"programs": { "programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": { "panels-attached-to-bar": {
"description": "Cuando está habilitado, los paneles se adjuntarán a la barra con un hermoso diseño de esquina invertida.", "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" "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": { "lock-screen": {

View File

@@ -226,13 +226,17 @@
}, },
"floating": { "floating": {
"label": "Barre flottante", "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": { "margins": {
"label": "Marges", "label": "Marges",
"description": "Ajustez les marges autour de la barre flottante.", "description": "Ajustez les marges autour de la barre flottante.",
"vertical": "Verticale", "vertical": "Verticale",
"horizontal": "Horizontale" "horizontal": "Horizontale"
},
"outer-corners": {
"description": "Affiche des coins incurvés vers l'extérieur sur la barre.",
"label": "Coins extérieurs"
} }
}, },
"widgets": { "widgets": {
@@ -584,6 +588,10 @@
"foot": { "foot": {
"description": "Écrire ~/.config/foot/themes/noctalia et recharger", "description": "Écrire ~/.config/foot/themes/noctalia et recharger",
"description-missing": "Nécessite que le terminal foot soit installé" "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": { "programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": { "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é.", "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." "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": { "lock-screen": {

View File

@@ -226,13 +226,17 @@
}, },
"floating": { "floating": {
"label": "Barra flutuante", "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": { "margins": {
"label": "Margens", "label": "Margens",
"description": "Ajuste as margens ao redor da barra flutuante.", "description": "Ajuste as margens ao redor da barra flutuante.",
"vertical": "Vertical", "vertical": "Vertical",
"horizontal": "Horizontal" "horizontal": "Horizontal"
},
"outer-corners": {
"description": "Exibe cantos curvados para fora na barra.",
"label": "Cantos externos"
} }
}, },
"widgets": { "widgets": {
@@ -546,6 +550,10 @@
"foot": { "foot": {
"description": "Escrever {filepath} e recarregar", "description": "Escrever {filepath} e recarregar",
"description-missing": "Requer que o {app} esteja instalado" "description-missing": "Requer que o {app} esteja instalado"
},
"alacritty": {
"description": "Escreva {filepath} e recarregue.",
"description-missing": "Requer que o {app} esteja instalado."
} }
}, },
"programs": { "programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": { "panels-attached-to-bar": {
"description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.", "description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.",
"label": "Anexar painéis à barra" "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": { "lock-screen": {

View File

@@ -226,13 +226,17 @@
}, },
"floating": { "floating": {
"label": "浮动状态栏", "label": "浮动状态栏",
"description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。" "description": "将工具栏显示为浮动的“药丸”形状。"
}, },
"margins": { "margins": {
"label": "边距", "label": "边距",
"description": "调整浮动状态栏周围的边距。", "description": "调整浮动状态栏周围的边距。",
"vertical": "垂直", "vertical": "垂直",
"horizontal": "水平" "horizontal": "水平"
},
"outer-corners": {
"description": "在栏上显示向外弯曲的角。",
"label": "外角"
} }
}, },
"widgets": { "widgets": {
@@ -584,6 +588,10 @@
"foot": { "foot": {
"description": "写入 {filepath} 并重新加载", "description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}" "description-missing": "需要安装 {app}"
},
"alacritty": {
"description": "写入 {filepath} 并重新加载",
"description-missing": "需要安装 {app}"
} }
}, },
"programs": { "programs": {
@@ -872,6 +880,10 @@
"panels-attached-to-bar": { "panels-attached-to-bar": {
"description": "启用后,面板将以美观的倒角设计附加到栏上。", "description": "启用后,面板将以美观的倒角设计附加到栏上。",
"label": "将面板连接到杆上" "label": "将面板连接到杆上"
},
"dim-desktop": {
"description": "当面板或菜单打开时,桌面变暗。",
"label": "昏暗的桌面"
} }
}, },
"lock-screen": { "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) // Debug log (only when Settings.isDebug is true)
function d(...args) { function d(...args) {
if (Settings && Settings.isDebug) { if (Settings && Settings.isDebug) {
var msg = _formatMessage(...args) 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) { function w(...args) {
var msg = _formatMessage(...args) var msg = _formatMessage(...args)
console.warn(msg) console.warn(msg)
} }
// Error log // Error log (always visible)
function e(...args) { function e(...args) {
var msg = _formatMessage(...args) var msg = _formatMessage(...args)
console.error(msg) console.error(msg)

View File

@@ -146,6 +146,12 @@ Singleton {
property real marginVertical: 0.25 property real marginVertical: 0.25
property real marginHorizontal: 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 // Widget configuration for modular bar system
property JsonObject widgets property JsonObject widgets
widgets: JsonObject { widgets: JsonObject {
@@ -182,6 +188,7 @@ Singleton {
// general // general
property JsonObject general: JsonObject { property JsonObject general: JsonObject {
property string avatarImage: "" property string avatarImage: ""
property bool dimDesktop: true
property bool showScreenCorners: false property bool showScreenCorners: false
property bool forceBlackScreenCorners: false property bool forceBlackScreenCorners: false
property real scaleRatio: 1.0 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 preferredWidth: 380 * Style.uiScaleRatio
preferredHeight: 500 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio
panelKeyboardFocus: true
// Connections to update local volumes when AudioService changes // Connections to update local volumes when AudioService changes
Connections { Connections {

View File

@@ -10,60 +10,92 @@ import qs.Widgets
import qs.Modules.Notification import qs.Modules.Notification
import qs.Modules.Bar.Extras import qs.Modules.Bar.Extras
Variants { // Bar Component
model: Quickshell.screens Item {
id: root
delegate: Loader { // This property will be set by NFullScreenWindow
id: root 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 { // Fill the parent (the Loader)
screen: modelData || null 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 // Wait for screen to be set before loading bar content
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Style.barHeight : screen.width Loader {
color: Color.transparent id: barContentLoader
anchors.fill: parent
active: root.screen !== null && root.screen !== undefined
anchors { sourceComponent: Item {
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" anchors.fill: parent
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"
}
// Floating bar margins - only apply when floating is enabled // Background fill with shadow
// 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. NShapedRectangle {
margins { id: bar
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
}
Component.onCompleted: { // Position and size the bar based on orientation and floating margins
if (modelData && modelData.name) { x: (root.barPosition === "right") ? (parent.width - Style.barHeight - root.barMarginH) : root.barMarginH
BarService.registerBar(modelData.name) 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 { backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
anchors.fill: parent
clip: true
// Background fill with shadow // Floating bar rounded corners
Rectangle { topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0
id: bar 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 topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical"
topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")
topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical"
// Floating bar rounded corners bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")
radius: Settings.data.bar.floating ? Style.radiusL : 0 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 { MouseArea {
@@ -73,8 +105,11 @@ Variants {
preventStealing: true preventStealing: true
onClicked: function (mouse) { onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
// Important to pass the screen here so we get the right widget for the actual bar that was clicked. // Look up for any ControlCenter button on this bar
controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter", screen.name)) 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 mouse.accepted = true
} }
} }
@@ -84,168 +119,188 @@ Variants {
anchors.fill: parent anchors.fill: parent
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
} }
}
}
}
// For vertical bars // For vertical bars
Component { Component {
id: verticalBarComponent id: verticalBarComponent
Item { Item {
anchors.fill: parent anchors.fill: parent
clip: true
// Top section (left widgets) // Top section (left widgets)
ColumnLayout { ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.marginM anchors.topMargin: Style.marginM
spacing: Style.marginS spacing: Style.marginS
Repeater { Repeater {
model: Settings.data.bar.widgets.left model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader { delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "") required property var modelData
barDensity: Settings.data.bar.density required property int index
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
// Center section (center widgets) widgetId: modelData.id || ""
ColumnLayout { barDensity: Settings.data.bar.density
anchors.horizontalCenter: parent.horizontalCenter widgetScreen: root.screen
anchors.verticalCenter: parent.verticalCenter widgetProps: ({
spacing: Style.marginS "widgetId": modelData.id,
"section": "left",
Repeater { "sectionWidgetIndex": index,
model: Settings.data.bar.widgets.center "sectionWidgetsCount": Settings.data.bar.widgets.left.length
delegate: BarWidgetLoader { })
widgetId: (modelData.id !== undefined ? modelData.id : "") Layout.alignment: Qt.AlignHCenter
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
}
}
}
} }
} }
}
// For horizontal bars // Center section (center widgets)
Component { ColumnLayout {
id: horizontalBarComponent anchors.horizontalCenter: parent.horizontalCenter
Item { anchors.verticalCenter: parent.verticalCenter
anchors.fill: parent spacing: Style.marginS
// Left Section Repeater {
RowLayout { model: Settings.data.bar.widgets.center
id: leftSection delegate: BarWidgetLoader {
objectName: "leftSection" required property var modelData
anchors.left: parent.left required property int index
anchors.leftMargin: Style.marginS
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS
Repeater { widgetId: modelData.id || ""
model: Settings.data.bar.widgets.left barDensity: Settings.data.bar.density
delegate: BarWidgetLoader { widgetScreen: root.screen
widgetId: (modelData.id !== undefined ? modelData.id : "") widgetProps: ({
barDensity: Settings.data.bar.density "widgetId": modelData.id,
widgetProps: { "section": "center",
"screen": root.modelData || null, "sectionWidgetIndex": index,
"widgetId": modelData.id, "sectionWidgetsCount": Settings.data.bar.widgets.center.length
"section": "left", })
"sectionWidgetIndex": index, Layout.alignment: Qt.AlignHCenter
"sectionWidgetsCount": Settings.data.bar.widgets.left.length }
} }
Layout.alignment: Qt.AlignVCenter }
}
}
}
// Center Section // Bottom section (right widgets)
RowLayout { ColumnLayout {
id: centerSection anchors.horizontalCenter: parent.horizontalCenter
objectName: "centerSection" anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: Style.marginM
anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS
spacing: Style.marginS
Repeater { Repeater {
model: Settings.data.bar.widgets.center model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader { delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "") required property var modelData
barDensity: Settings.data.bar.density required property int index
widgetProps: {
"screen": root.modelData || null,
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
// Right Section widgetId: modelData.id || ""
RowLayout { barDensity: Settings.data.bar.density
id: rightSection widgetScreen: root.screen
objectName: "rightSection" widgetProps: ({
anchors.right: parent.right "widgetId": modelData.id,
anchors.rightMargin: Style.marginS "section": "right",
anchors.verticalCenter: parent.verticalCenter "sectionWidgetIndex": index,
spacing: Style.marginS "sectionWidgetsCount": Settings.data.bar.widgets.right.length
})
Layout.alignment: Qt.AlignHCenter
}
}
}
}
}
Repeater { // For horizontal bars
model: Settings.data.bar.widgets.right Component {
delegate: BarWidgetLoader { id: horizontalBarComponent
widgetId: (modelData.id !== undefined ? modelData.id : "") Item {
barDensity: Settings.data.bar.density anchors.fill: parent
widgetProps: { clip: true
"screen": root.modelData || null,
"widgetId": modelData.id, // Left Section
"section": "right", RowLayout {
"sectionWidgetIndex": index, id: leftSection
"sectionWidgetsCount": Settings.data.bar.widgets.right.length objectName: "leftSection"
} anchors.left: parent.left
Layout.alignment: Qt.AlignVCenter 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 id: root
preferredWidth: 350 * Style.uiScaleRatio preferredWidth: 350 * Style.uiScaleRatio
preferredHeight: 250 * Style.uiScaleRatio preferredHeight: 210 * Style.uiScaleRatio
panelKeyboardFocus: true
property var optionsModel: [] property var optionsModel: []

View File

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

View File

@@ -14,7 +14,8 @@ NPanel {
property ShellScreen screen property ShellScreen screen
readonly property var now: Time.date readonly property var now: Time.date
panelKeyboardFocus: true preferredWidth: 500
preferredHeight: 700
// Helper function to calculate ISO week number // Helper function to calculate ISO week number
function getISOWeekNumber(date) { function getISOWeekNumber(date) {
@@ -44,7 +45,8 @@ NPanel {
return numWeeks * rowHeight 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 { ColumnLayout {
id: content id: content
@@ -65,20 +67,6 @@ NPanel {
isCurrentMonth = checkIsCurrentMonth() isCurrentMonth = checkIsCurrentMonth()
} }
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
context: Qt.WidgetShortcut
enabled: root.opened
}
Connections { Connections {
target: Time target: Time
function onDateChanged() { function onDateChanged() {
@@ -623,7 +611,7 @@ NPanel {
onClicked: { onClicked: {
const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}` 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]) Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes])
PanelService.getPanel("calendarPanel").toggle(null) root.close()
} }
onExited: { onExited: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
onEntered: { onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) { if (!PanelService.getPanel("calendarPanel", screen)?.active) {
TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection()) TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
} }
} }
@@ -130,7 +130,7 @@ Rectangle {
} }
onClicked: { onClicked: {
TooltipService.hide() 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 colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mHover
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this) onClicked: PanelService.getPanel("controlCenterPanel", screen)?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle()
IconImage { IconImage {
id: customOrDistroLogo id: customOrDistroLogo

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,5 +20,5 @@ NIconButton {
colorFg: Color.mError colorFg: Color.mError
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: 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() { function open() {
visible = true visible = true
PanelService.willOpenPanel(trayPanel)
} }
function close() { function close() {

View File

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

View File

@@ -20,5 +20,5 @@ NIconButton {
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: 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 autoHide: false
forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) onRightClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this)
tooltipText: { tooltipText: {
if (pill.text !== "") { if (pill.text !== "") {
return pill.text return pill.text

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,5 +9,7 @@ NIconButtonHot {
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
tooltipText: I18n.tr("quickSettings.bluetooth.tooltip.action") 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: { onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open() settingsPanel.open()
} }

View File

@@ -10,6 +10,6 @@ NIconButtonHot {
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
hot: Settings.data.notifications.doNotDisturb hot: Settings.data.notifications.doNotDisturb
tooltipText: I18n.tr("quickSettings.notifications.tooltip.action") 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 onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
} }

View File

@@ -14,7 +14,7 @@ NIconButtonHot {
enabled: hasPP enabled: hasPP
icon: PowerProfileService.getIcon() icon: PowerProfileService.getIcon()
hot: !PowerProfileService.isDefault() 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: { onClicked: {
PowerProfileService.cycleProfile() PowerProfileService.cycleProfile()
} }

View File

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

View File

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

View File

@@ -29,5 +29,5 @@ NIconButtonHot {
} }
tooltipText: I18n.tr("quickSettings.wifi.tooltip.action") 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 preferredWidthRatio: 0.3
preferredHeightRatio: 0.5 preferredHeightRatio: 0.5
panelKeyboardFocus: true
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity)
panelKeyboardFocus: true // Needs Exclusive focus for text input
// Positioning // Positioning
readonly property string launcherPosition: Settings.data.appLauncher.position 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 badgeSize: Math.round(Style.baseWidgetSize * 1.6)
readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) 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 // Public API for plugins
function setSearchText(text) { function setSearchText(text) {
searchText = 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 // UI
panelContent: Rectangle { panelContent: Rectangle {
id: ui 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Style.animationFast 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL anchors.margins: Style.marginL
@@ -319,36 +329,8 @@ NPanel {
onTextChanged: searchText = text onTextChanged: searchText = text
Component.onCompleted: { Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) { if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus() 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 => { onClicked: mouse => {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
selectedIndex = index selectedIndex = index
ui.activate() root.activate()
mouse.accepted = true mouse.accepted = true
} }
} }

View File

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

View File

@@ -15,9 +15,9 @@ NPanel {
preferredWidth: 400 * Style.uiScaleRatio preferredWidth: 400 * Style.uiScaleRatio
preferredHeight: 340 * Style.uiScaleRatio preferredHeight: 340 * Style.uiScaleRatio
panelAnchorHorizontalCenter: true panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties // Timer properties
property int timerDuration: 9000 // 9 seconds 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 // Countdown timer
Timer { Timer {
id: countdownTimer id: countdownTimer
@@ -165,81 +211,6 @@ NPanel {
id: ui id: ui
color: Color.transparent 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 // Navigation functions
function selectFirst() { function selectFirst() {
root.selectFirst() root.selectFirst()

View File

@@ -16,9 +16,6 @@ NPanel {
panelAnchorHorizontalCenter: true panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true panelAnchorVerticalCenter: true
panelKeyboardFocus: true
draggable: !PanelService.hasOpenedPopup
// Tabs enumeration, order is NOT relevant // Tabs enumeration, order is NOT relevant
enum Tab { 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 { panelContent: Rectangle {
color: Color.transparent color: Color.transparent
@@ -296,62 +326,6 @@ NPanel {
anchors.margins: Style.marginL anchors.margins: Style.marginL
spacing: 0 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 // Main content area
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true

View File

@@ -25,7 +25,7 @@ ColumnLayout {
// Handler for drag start - disables panel background clicks // Handler for drag start - disables panel background clicks
function handleDragStart() { function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel") var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.disableBackgroundClick) { if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick() panel.disableBackgroundClick()
} }
@@ -33,7 +33,7 @@ ColumnLayout {
// Handler for drag end - re-enables panel background clicks // Handler for drag end - re-enables panel background clicks
function handleDragEnd() { function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel") var panel = PanelService.getPanel("settingsPanel", screen)
if (panel && panel.enableBackgroundClick) { if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick() panel.enableBackgroundClick()
} }
@@ -102,6 +102,14 @@ ColumnLayout {
onToggled: checked => Settings.data.bar.floating = checked 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 // Floating bar options - only show when floating is enabled
ColumnLayout { ColumnLayout {
visible: Settings.data.bar.floating visible: Settings.data.bar.floating

View File

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

View File

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

View File

@@ -26,6 +26,13 @@ ColumnLayout {
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked 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 { NToggle {
label: I18n.tr("settings.user-interface.panels-attached-to-bar.label") label: I18n.tr("settings.user-interface.panels-attached-to-bar.label")
description: I18n.tr("settings.user-interface.panels-attached-to-bar.description") description: I18n.tr("settings.user-interface.panels-attached-to-bar.description")

View File

@@ -14,17 +14,17 @@ NPanel {
preferredHeight: 600 * Style.uiScaleRatio preferredHeight: 600 * Style.uiScaleRatio
preferredWidthRatio: 0.4 preferredWidthRatio: 0.4
preferredHeightRatio: 0.6 preferredHeightRatio: 0.6
panelAnchorHorizontalCenter: true panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Prevent closing during setup
backgroundClickEnabled: false
draggable: false
property int currentStep: 0 property int currentStep: 0
property int totalSteps: 5 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 // Setup wizard data
property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory
property string selectedWallpaper: "" property string selectedWallpaper: ""
@@ -42,17 +42,6 @@ NPanel {
anchors.margins: Style.marginXL anchors.margins: Style.marginXL
spacing: Style.marginL 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 // Step content - takes most of the space
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true

View File

@@ -12,15 +12,87 @@ import "../../Helpers/FuzzySort.js" as FuzzySort
NPanel { NPanel {
id: root id: root
preferredWidth: 640 * Style.uiScaleRatio preferredWidth: 800 * Style.uiScaleRatio
preferredHeight: 480 * Style.uiScaleRatio preferredHeight: 600 * Style.uiScaleRatio
preferredWidthRatio: 0.4 preferredWidthRatio: 0.5
preferredHeightRatio: 0.52 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 { panelContent: Rectangle {
id: wallpaperPanel id: wallpaperPanel
@@ -37,9 +109,31 @@ NPanel {
} }
property var currentScreen: Quickshell.screens[currentScreenIndex] property var currentScreen: Quickshell.screens[currentScreenIndex]
property string filterText: "" property string filterText: ""
property alias screenRepeater: screenRepeater
Component.onCompleted: {
root.contentItem = wallpaperPanel
}
color: Color.transparent 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 // Debounce timer for search
Timer { Timer {
id: searchDebounceTimer id: searchDebounceTimer
@@ -85,7 +179,7 @@ NPanel {
tooltipText: I18n.tr("settings.wallpaper.settings.section.label") tooltipText: I18n.tr("settings.wallpaper.settings.section.label")
baseSize: Style.baseWidgetSize * 0.8 baseSize: Style.baseWidgetSize * 0.8
onClicked: { onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper
settingsPanel.open() settingsPanel.open()
} }
@@ -324,7 +418,7 @@ NPanel {
model: filteredWallpapers model: filteredWallpapers
property int columns: 4 property int columns: 5
property int itemSize: cellWidth property int itemSize: cellWidth
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)

View File

@@ -62,7 +62,7 @@ Singleton {
} else { } else {
BatteryService.initialSetter = true BatteryService.initialSetter = true
ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup")) 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 uninstallerProcess.running = true
} }
} }
@@ -123,7 +123,7 @@ Singleton {
Settings.data.battery.chargingMode = BatteryService.chargingMode Settings.data.battery.chargingMode = BatteryService.chargingMode
} else if (exitCode === 2) { } else if (exitCode === 2) {
ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup")) 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() BatteryService.runInstaller()
} else { } else {
ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed")) 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 { Singleton {
id: root 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 var values: Array(barsCount).fill(0)
property int barsCount: 48 property int barsCount: 48
property var config: ({ property var config: ({

View File

@@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons import qs.Commons
import qs.Services import qs.Services
@@ -27,7 +28,10 @@ Item {
IpcHandler { IpcHandler {
target: "settings" target: "settings"
function toggle() { function toggle() {
settingsPanel.toggle() root.withTargetScreen(screen => {
var settingsPanel = PanelService.getPanel("settingsPanel", screen)
settingsPanel.toggle()
})
} }
} }
@@ -35,7 +39,10 @@ Item {
target: "notifications" target: "notifications"
function toggleHistory() { function toggleHistory() {
// Will attempt to open the panel next to the bar button if any. // 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() { function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
@@ -63,15 +70,24 @@ Item {
IpcHandler { IpcHandler {
target: "launcher" target: "launcher"
function toggle() { function toggle() {
launcherPanel.toggle() root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.toggle()
})
} }
function clipboard() { function clipboard() {
launcherPanel.setSearchText(">clip ") root.withTargetScreen(screen => {
launcherPanel.toggle() var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.setSearchText(">clip ")
launcherPanel.toggle()
})
} }
function calculator() { function calculator() {
launcherPanel.setSearchText(">calc ") root.withTargetScreen(screen => {
launcherPanel.toggle() var launcherPanel = PanelService.getPanel("launcherPanel", screen)
launcherPanel.setSearchText(">calc ")
launcherPanel.toggle()
})
} }
} }
@@ -158,7 +174,10 @@ Item {
IpcHandler { IpcHandler {
target: "sessionMenu" target: "sessionMenu"
function toggle() { function toggle() {
sessionMenuPanel.toggle() root.withTargetScreen(screen => {
var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen)
sessionMenuPanel.toggle()
})
} }
function lockAndSuspend() { function lockAndSuspend() {
@@ -170,7 +189,10 @@ Item {
target: "controlCenter" target: "controlCenter"
function toggle() { function toggle() {
// Will attempt to open the panel next to the bar button if any. // 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" target: "wallpaper"
function toggle() { function toggle() {
if (Settings.data.wallpaper.enabled) { if (Settings.data.wallpaper.enabled) {
wallpaperPanel.toggle() root.withTargetScreen(screen => {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen)
wallpaperPanel.toggle()
})
} }
} }
@@ -228,6 +253,7 @@ Item {
} }
} }
} }
IpcHandler { IpcHandler {
target: "powerProfile" target: "powerProfile"
function cycle() { function cycle() {
@@ -248,6 +274,7 @@ Item {
} }
} }
} }
IpcHandler { IpcHandler {
target: "media" target: "media"
function playPause() { function playPause() {
@@ -292,4 +319,83 @@ Item {
MediaService.seekByRatio(positionVal) 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 repeat: true
running: true running: true
onTriggered: { onTriggered: {
Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) //Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused)
if (autoSwitchingPaused) if (autoSwitchingPaused)
return return
// Only update if we don't have a playing player or if current player is paused // 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 registeredPanels: ({})
property var openedPanel: null property var openedPanel: null
signal willOpen signal willOpen
signal didClose
// Currently opened popups, can have more than one. // Currently opened popups, can have more than one.
// ex: when opening an NIconPicker from a widget setting. // ex: when opening an NIconPicker from a widget setting.
@@ -21,15 +22,53 @@ Singleton {
property bool hasOpenedPopup: false property bool hasOpenedPopup: false
signal popupChanged signal popupChanged
// Register this panel // Registered panel loaders (before they're loaded)
function registerPanel(panel) { property var registeredPanelLoaders: ({})
registeredPanels[panel.objectName] = panel
Logger.d("PanelService", "Registered:", panel.objectName) // 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 // Register this panel (called after panel is loaded)
function getPanel(name) { function registerPanel(panel) {
return registeredPanels[name] || null 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 // Check if a panel exists
@@ -52,6 +91,9 @@ Singleton {
if (openedPanel && openedPanel === panel) { if (openedPanel && openedPanel === panel) {
openedPanel = null openedPanel = null
} }
// emit signal
didClose()
} }
// Popups // 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
import QtQuick.Effects
import qs.Commons import qs.Commons
Item { Item {
@@ -24,8 +25,17 @@ Item {
property string bottomLeftInvertedDirection: "horizontal" // default: curves left property string bottomLeftInvertedDirection: "horizontal" // default: curves left
property string bottomRightInvertedDirection: "horizontal" // default: curves right property string bottomRightInvertedDirection: "horizontal" // default: curves right
// Background color // Background color and borders
property color backgroundColor: "white" 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 // Check if any corner is inverted
readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted 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 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) readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0)
// Simple rectangle for non-inverted corners (better performance) // Background layer: shape with shadow effects (layer.enabled)
Rectangle { Item {
id: simpleBackground id: shadowLayer
anchors.fill: parent anchors.fill: parent
color: root.backgroundColor z: 0
radius: topLeftRadius // Use topLeftRadius as default
border.width: Style.borderS
border.color: Color.mOutline
visible: !root.hasInvertedCorners
topLeftRadius: root.topLeftRadius // Apply shadow effect to this layer only
topRightRadius: root.topRightRadius layer.enabled: root.shadowEnabled
bottomLeftRadius: root.bottomLeftRadius layer.smooth: true
bottomRightRadius: root.bottomRightRadius // 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) // Simple rectangle for non-inverted corners (better performance)
Canvas { Rectangle {
id: background id: simpleBackground
anchors.fill: parent anchors.fill: parent
anchors.topMargin: -root.topPadding color: root.backgroundColor
anchors.bottomMargin: -root.bottomPadding radius: topLeftRadius // Use topLeftRadius as default
anchors.leftMargin: -root.leftPadding border.width: borderWidth
anchors.rightMargin: -root.rightPadding border.color: borderColor
visible: root.hasInvertedCorners visible: !root.hasInvertedCorners
antialiasing: true topLeftRadius: root.topLeftRadius
renderTarget: Canvas.FramebufferObject topRightRadius: root.topRightRadius
smooth: true bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
onPaint: { // Background with custom corners (for inverted corners)
var ctx = getContext("2d") Canvas {
ctx.reset() 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 antialiasing: true
var x = root.leftPadding renderTarget: Canvas.FramebufferObject
var y = root.topPadding smooth: true
var w = width - root.leftPadding - root.rightPadding
var h = height - root.topPadding - root.bottomPadding
ctx.fillStyle = root.backgroundColor onPaint: {
ctx.beginPath() var ctx = getContext("2d")
ctx.reset()
// Start from top left // Adjust coordinates to account for inverted corner padding
if (topLeftInverted) { var x = root.leftPadding
if (topLeftInvertedDirection === "vertical") { var y = root.topPadding
ctx.moveTo(x, y) 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 { } else {
ctx.moveTo(x + topLeftRadius, y) ctx.moveTo(x + topLeftRadius, y)
} }
} else {
ctx.moveTo(x + topLeftRadius, y)
}
// Top edge and top right corner // Top edge and top right corner
if (topRightInverted) { if (topRightInverted) {
if (topRightInvertedDirection === "horizontal") { if (topRightInvertedDirection === "horizontal") {
// Curves to the right // Curves to the right
ctx.lineTo(x + w, y) ctx.lineTo(x + w, y)
ctx.lineTo(x + w + topRightRadius, y) ctx.lineTo(x + w + topRightRadius, y)
ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) 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 { } else {
// Curves upward ctx.lineTo(x + w - topRightRadius, y)
ctx.lineTo(x + w, y) ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
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 {
ctx.lineTo(x + w - topRightRadius, y)
ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
}
// Right edge and bottom right corner // Right edge and bottom right corner
if (bottomRightInverted) { if (bottomRightInverted) {
if (bottomRightInvertedDirection === "horizontal") { if (bottomRightInvertedDirection === "horizontal") {
// Curves to the right // 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.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)
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.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius)
}
// Bottom edge and bottom left corner // Bottom edge and bottom left corner
if (bottomLeftInverted) { if (bottomLeftInverted) {
if (bottomLeftInvertedDirection === "horizontal") { if (bottomLeftInvertedDirection === "horizontal") {
// Curves to the left // 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.lineTo(x - bottomLeftRadius, y + h) ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius)
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.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius)
}
// Left edge and back to top left corner // Left edge and back to top left corner
if (topLeftInverted) { if (topLeftInverted) {
if (topLeftInvertedDirection === "horizontal") { if (topLeftInvertedDirection === "horizontal") {
// Curves to the left // Curves to the left
ctx.lineTo(x, y + topLeftRadius) ctx.lineTo(x, y + topLeftRadius)
ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) ctx.quadraticCurveTo(x, y, x - topLeftRadius, y)
ctx.lineTo(x, y) ctx.lineTo(x, y)
ctx.lineTo(x + topLeftRadius, 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 { } else {
// Curves upward
ctx.lineTo(x, y + topLeftRadius) ctx.lineTo(x, y + topLeftRadius)
ctx.lineTo(x, y) ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
ctx.lineTo(x, y - topLeftRadius)
ctx.quadraticCurveTo(x, y, x + topLeftRadius, y)
} }
} else {
ctx.lineTo(x, y + topLeftRadius)
ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
}
ctx.closePath() ctx.closePath()
ctx.fill() 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 // Trigger repaint when properties change
onTopLeftRadiusChanged: background.requestPaint() onTopLeftRadiusChanged: background.requestPaint()
onTopRightRadiusChanged: 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 { Loader {
active: i18nLoaded && settingsLoaded active: i18nLoaded && settingsLoaded
@@ -99,8 +166,7 @@ ShellRoot {
Background {} Background {}
Overview {} Overview {}
ScreenCorners {}
Bar {}
Dock {} Dock {}
Notification { Notification {
@@ -121,67 +187,118 @@ ShellRoot {
// IPCService is treated as a service // IPCService is treated as a service
// but it's actually an Item that needs to exists in the shell. // but it's actually an Item that needs to exists in the shell.
IPCService {} IPCService {}
}
}
// ------------------------------ // ------------------------------
// All the NPanels // NFullScreenWindow for each screen (manages bar + all panels)
Launcher { // Wrapped in Loader to optimize memory - only loads when screen needs it
id: launcherPanel Variants {
objectName: "launcherPanel" 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 { property bool windowLoaded: false
id: controlCenterPanel
objectName: "controlCenterPanel" 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 { // BarExclusionZone - created after NFullScreenWindow has fully loaded
id: calendarPanel // Must also be disabled when bar is disabled (follows shouldBeActive)
objectName: "calendarPanel" Loader {
} active: parent.windowLoaded && parent.shouldBeActive
asynchronous: false
SettingsPanel { sourceComponent: BarExclusionZone {
id: settingsPanel screen: modelData
objectName: "settingsPanel" }
}
DirectWidgetSettingsPanel { onLoaded: {
id: directWidgetSettingsPanel Logger.d("Shell", "BarExclusionZone created for", modelData?.name)
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"
} }
} }
} }