diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f9224eef..4949b25f 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Schwebende Statusleiste", - "description": "Statusleiste als schwebende 'Pille' anzeigen. Hinweis: Dies verschiebt die Bildschirmecken an die Ränder." + "description": "Statusleiste als schwebende 'Pille' anzeigen." }, "margins": { "label": "Ränder", "description": "Ränder um die schwebende Statusleiste anpassen.", "vertical": "Vertikal", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Zeigt nach außen gewölbte Ecken auf der Leiste an.", + "label": "Äußere Ecken" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Schreibt {filepath} und lädt neu", "description-missing": "Erfordert {app} Terminal" + }, + "alacritty": { + "description": "Schreibe {Dateipfad} und lade neu", + "description-missing": "Benötigt die Installation von {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Wenn aktiviert, werden die Panels mit einem schönen, umgekehrten Eckdesign an der Leiste befestigt.", "label": "Paneele an Stange befestigen" + }, + "dim-desktop": { + "description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.", + "label": "Dimmer Schreibtisch" } }, "lock-screen": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 76b87f64..8bee2a80 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -226,7 +226,11 @@ }, "floating": { "label": "Floating bar", - "description": "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges." + "description": "Displays the bar as a floating 'pill'." + }, + "outer-corners": { + "label": "Outer corners", + "description": "Displays outwardly curved corners on the bar." }, "margins": { "label": "Margins", @@ -577,6 +581,10 @@ "terminal": { "label": "Terminal", "description": "Terminal emulator theming.", + "alacritty": { + "description": "Write {filepath} and reload", + "description-missing": "Requires {app} to be installed" + }, "kitty": { "description": "Write {filepath} and reload", "description-missing": "Requires {app} to be installed" @@ -851,6 +859,10 @@ "description": "Changes the size of the general user interface, excluding the bar.", "reset-scaling": "Reset interface scaling" }, + "dim-desktop": { + "label": "Dim desktop", + "description": "Dim the desktop when panels or menus are open." + }, "border-radius": { "label": "Border radius", "description": "Controls the corner roundness of windows, buttons, and other elements.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 7252dbf9..e4eb12b6 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barra flotante", - "description": "Muestra la barra como una 'píldora' flotante. Nota: Esto moverá las esquinas de la pantalla a los bordes." + "description": "Muestra la barra como una 'píldora' flotante." }, "margins": { "label": "Márgenes", "description": "Ajusta los márgenes alrededor de la barra flotante.", "vertical": "Vertical", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Muestra esquinas curvadas hacia afuera en la barra.", + "label": "Esquinas exteriores" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Escribir {filepath} y recargar", "description-missing": "Requiere que {app} esté instalado" + }, + "alacritty": { + "description": "Escribe {filepath} y recarga", + "description-missing": "Requiere que {app} esté instalado/a." } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Cuando está habilitado, los paneles se adjuntarán a la barra con un hermoso diseño de esquina invertida.", "label": "Adjuntar paneles a la barra" + }, + "dim-desktop": { + "description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", + "label": "Dim escritorio" } }, "lock-screen": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1e0b879d..cadc7836 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barre flottante", - "description": "Affiche la barre sous forme de 'pilule' flottante. Note : Ceci déplacera les coins de l'écran vers les bords." + "description": "Affiche la barre sous forme de 'pilule' flottante." }, "margins": { "label": "Marges", "description": "Ajustez les marges autour de la barre flottante.", "vertical": "Verticale", "horizontal": "Horizontale" + }, + "outer-corners": { + "description": "Affiche des coins incurvés vers l'extérieur sur la barre.", + "label": "Coins extérieurs" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Écrire ~/.config/foot/themes/noctalia et recharger", "description-missing": "Nécessite que le terminal foot soit installé" + }, + "alacritty": { + "description": "Écrire {filepath} et recharger.", + "description-missing": "Nécessite l'installation de {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Lorsque cette option est activée, les panneaux seront attachés à la barre avec un design élégant de coin inversé.", "label": "Fixer les panneaux à la barre." + }, + "dim-desktop": { + "description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", + "label": "Dim bureau" } }, "lock-screen": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 7b3722cd..41ca38e0 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barra flutuante", - "description": "Exibe a barra como uma 'pílula' flutuante. Nota: Isso moverá os cantos da tela para as bordas." + "description": "Exibe a barra como uma 'pílula' flutuante." }, "margins": { "label": "Margens", "description": "Ajuste as margens ao redor da barra flutuante.", "vertical": "Vertical", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Exibe cantos curvados para fora na barra.", + "label": "Cantos externos" } }, "widgets": { @@ -546,6 +550,10 @@ "foot": { "description": "Escrever {filepath} e recarregar", "description-missing": "Requer que o {app} esteja instalado" + }, + "alacritty": { + "description": "Escreva {filepath} e recarregue.", + "description-missing": "Requer que o {app} esteja instalado." } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.", "label": "Anexar painéis à barra" + }, + "dim-desktop": { + "description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", + "label": "Dim área de trabalho" } }, "lock-screen": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d107d181..11a1b176 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -226,13 +226,17 @@ }, "floating": { "label": "浮动状态栏", - "description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。" + "description": "将工具栏显示为浮动的“药丸”形状。" }, "margins": { "label": "边距", "description": "调整浮动状态栏周围的边距。", "vertical": "垂直", "horizontal": "水平" + }, + "outer-corners": { + "description": "在栏上显示向外弯曲的角。", + "label": "外角" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "写入 {filepath} 并重新加载", "description-missing": "需要安装 {app}" + }, + "alacritty": { + "description": "写入 {filepath} 并重新加载", + "description-missing": "需要安装 {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "启用后,面板将以美观的倒角设计附加到栏上。", "label": "将面板连接到杆上" + }, + "dim-desktop": { + "description": "当面板或菜单打开时,桌面变暗。", + "label": "昏暗的桌面" } }, "lock-screen": { diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1fb05a83 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/Commons/Logger.qml b/Commons/Logger.qml index 5613d890..5c18b90b 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -25,27 +25,27 @@ Singleton { } } - // Info log (always visible) - function i(...args) { - var msg = _formatMessage(...args) - console.log(msg) - } - // Debug log (only when Settings.isDebug is true) function d(...args) { if (Settings && Settings.isDebug) { var msg = _formatMessage(...args) - console.log(msg) + console.debug(msg) } } - // Warning log + // Info log (always visible) + function i(...args) { + var msg = _formatMessage(...args) + console.info(msg) + } + + // Warning log (always visible) function w(...args) { var msg = _formatMessage(...args) console.warn(msg) } - // Error log + // Error log (always visible) function e(...args) { var msg = _formatMessage(...args) console.error(msg) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 1483a519..105b6df4 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -146,6 +146,12 @@ Singleton { property real marginVertical: 0.25 property real marginHorizontal: 0.25 + // Bar outer corners (inverted/concave corners at bar edges when not floating) + property bool outerCorners: true + + // Reserves space with compositor + property bool exclusive: true + // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { @@ -182,6 +188,7 @@ Singleton { // general property JsonObject general: JsonObject { property string avatarImage: "" + property bool dimDesktop: true property bool showScreenCorners: false property bool forceBlackScreenCorners: false property real scaleRatio: 1.0 diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml deleted file mode 100644 index 6c35b008..00000000 --- a/Modules/Background/ScreenCorners.qml +++ /dev/null @@ -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 - } - } - } -} diff --git a/Modules/Bar/Audio/AudioPanel.qml b/Modules/Bar/Audio/AudioPanel.qml index dc4e1779..7393f846 100644 --- a/Modules/Bar/Audio/AudioPanel.qml +++ b/Modules/Bar/Audio/AudioPanel.qml @@ -18,7 +18,6 @@ NPanel { preferredWidth: 380 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true // Connections to update local volumes when AudioService changes Connections { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 31504433..0de76baa 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -10,60 +10,92 @@ import qs.Widgets import qs.Modules.Notification import qs.Modules.Bar.Extras -Variants { - model: Quickshell.screens +// Bar Component +Item { + id: root - delegate: Loader { - id: root + // This property will be set by NFullScreenWindow + property ShellScreen screen: null - required property ShellScreen modelData + // Expose bar region for click-through mask + readonly property var barRegion: barContentLoader.item?.children[0] || null - active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + // Bar positioning properties + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - sourceComponent: PanelWindow { - screen: modelData || null + // Fill the parent (the Loader) + anchors.fill: parent - WlrLayershell.namespace: "noctalia-bar" + // Register bar when screen becomes available + onScreenChanged: { + if (screen && screen.name) { + Logger.d("Bar", "Bar screen set to:", screen.name) + Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating) + Logger.d("Bar", " Margins - H:", barMarginH, "V:", barMarginV) + BarService.registerBar(screen.name) + } + } - implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Style.barHeight - implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Style.barHeight : screen.width - color: Color.transparent + // Wait for screen to be set before loading bar content + Loader { + id: barContentLoader + anchors.fill: parent + active: root.screen !== null && root.screen !== undefined - anchors { - top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" - bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" - left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" - right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" - } + sourceComponent: Item { + anchors.fill: parent - // Floating bar margins - only apply when floating is enabled - // Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom. - margins { - top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL : 0 - bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL : 0 - left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - } + // Background fill with shadow + NShapedRectangle { + id: bar - Component.onCompleted: { - if (modelData && modelData.name) { - BarService.registerBar(modelData.name) - } - } + // Position and size the bar based on orientation and floating margins + x: (root.barPosition === "right") ? (parent.width - Style.barHeight - root.barMarginH) : root.barMarginH + y: (root.barPosition === "bottom") ? (parent.height - Style.barHeight - root.barMarginV) : root.barMarginV + width: root.barIsVertical ? Style.barHeight : (parent.width - root.barMarginH * 2) + height: root.barIsVertical ? (parent.height - root.barMarginV * 2) : Style.barHeight - Item { - anchors.fill: parent - clip: true + backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) - // Background fill with shadow - Rectangle { - id: bar + // Floating bar rounded corners + topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0 + topRightRadius: Settings.data.bar.floating || topRightInverted ? Style.radiusL : 0 + bottomLeftRadius: Settings.data.bar.floating || bottomLeftInverted ? Style.radiusL : 0 + bottomRightRadius: Settings.data.bar.floating || bottomRightInverted ? Style.radiusL : 0 - anchors.fill: parent - color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right") + topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" + topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left") + topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" - // Floating bar rounded corners - radius: Settings.data.bar.floating ? Style.radiusL : 0 + bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right") + bottomLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" + bottomRightInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left") + bottomRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" + + // No border on the bar + borderWidth: 0 + + // Shadow configuration + shadowEnabled: true + shadowBlur: 0.5 + // Fade shadow progressively when a panel is attached to the bar to avoid visual disconnection + // shadowOpacity: { + // if (PanelService.openedPanel && PanelService.openedPanel.attachedToBar) { + // // Fade shadow out as panel opens (animationProgress goes from 0 to 1) + // return 1.0 - PanelService.openedPanel.animationProgress + // } + // return 1.0 + // } + Behavior on shadowOpacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } } MouseArea { @@ -73,8 +105,11 @@ Variants { preventStealing: true onClicked: function (mouse) { if (mouse.button === Qt.RightButton) { - // Important to pass the screen here so we get the right widget for the actual bar that was clicked. - controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter", screen.name)) + // Look up for any ControlCenter button on this bar + var widget = BarService.lookupWidget("ControlCenter", root.screen.name) + + // Open the panel near the button if any + PanelService.getPanel("controlCenterPanel", root.screen)?.toggle(widget) mouse.accepted = true } } @@ -84,168 +119,188 @@ Variants { anchors.fill: parent sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent } + } + } + } - // For vertical bars - Component { - id: verticalBarComponent - Item { - anchors.fill: parent + // For vertical bars + Component { + id: verticalBarComponent + Item { + anchors.fill: parent + clip: true - // Top section (left widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Style.marginM - spacing: Style.marginS + // Top section (left widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Style.marginM + spacing: Style.marginS - Repeater { - model: Settings.data.bar.widgets.left - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "left", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } + Repeater { + model: Settings.data.bar.widgets.left + delegate: BarWidgetLoader { + required property var modelData + required property int index - // Center section (center widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS - - Repeater { - model: Settings.data.bar.widgets.center - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "center", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } - - // Bottom section (right widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.marginM - spacing: Style.marginS - - Repeater { - model: Settings.data.bar.widgets.right - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "right", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.right.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + }) + Layout.alignment: Qt.AlignHCenter } } + } - // For horizontal bars - Component { - id: horizontalBarComponent - Item { - anchors.fill: parent + // Center section (center widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS - // Left Section - RowLayout { - id: leftSection - objectName: "leftSection" - anchors.left: parent.left - anchors.leftMargin: Style.marginS - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + Repeater { + model: Settings.data.bar.widgets.center + delegate: BarWidgetLoader { + required property var modelData + required property int index - Repeater { - model: Settings.data.bar.widgets.left - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "left", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + }) + Layout.alignment: Qt.AlignHCenter + } + } + } - // Center Section - RowLayout { - id: centerSection - objectName: "centerSection" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + // Bottom section (right widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginM + spacing: Style.marginS - Repeater { - model: Settings.data.bar.widgets.center - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "center", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + Repeater { + model: Settings.data.bar.widgets.right + delegate: BarWidgetLoader { + required property var modelData + required property int index - // Right Section - RowLayout { - id: rightSection - objectName: "rightSection" - anchors.right: parent.right - anchors.rightMargin: Style.marginS - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + }) + Layout.alignment: Qt.AlignHCenter + } + } + } + } + } - Repeater { - model: Settings.data.bar.widgets.right - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "right", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.right.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + // For horizontal bars + Component { + id: horizontalBarComponent + Item { + anchors.fill: parent + clip: true + + // Left Section + RowLayout { + id: leftSection + objectName: "leftSection" + anchors.left: parent.left + anchors.leftMargin: Style.marginS + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.left + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Center Section + RowLayout { + id: centerSection + objectName: "centerSection" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.center + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Right Section + RowLayout { + id: rightSection + objectName: "rightSection" + anchors.right: parent.right + anchors.rightMargin: Style.marginS + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.right + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + }) + Layout.alignment: Qt.AlignVCenter } } } diff --git a/Modules/Bar/Battery/BatteryPanel.qml b/Modules/Bar/Battery/BatteryPanel.qml index 8f4d2161..4d359dce 100644 --- a/Modules/Bar/Battery/BatteryPanel.qml +++ b/Modules/Bar/Battery/BatteryPanel.qml @@ -11,8 +11,7 @@ NPanel { id: root preferredWidth: 350 * Style.uiScaleRatio - preferredHeight: 250 * Style.uiScaleRatio - panelKeyboardFocus: true + preferredHeight: 210 * Style.uiScaleRatio property var optionsModel: [] diff --git a/Modules/Bar/Bluetooth/BluetoothPanel.qml b/Modules/Bar/Bluetooth/BluetoothPanel.qml index 18563c0d..6255fe93 100644 --- a/Modules/Bar/Bluetooth/BluetoothPanel.qml +++ b/Modules/Bar/Bluetooth/BluetoothPanel.qml @@ -13,7 +13,6 @@ NPanel { preferredWidth: 420 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true panelContent: Rectangle { color: Color.transparent diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 7df32214..a2317d61 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -14,7 +14,8 @@ NPanel { property ShellScreen screen readonly property var now: Time.date - panelKeyboardFocus: true + preferredWidth: 500 + preferredHeight: 700 // Helper function to calculate ISO week number function getISOWeekNumber(date) { @@ -44,7 +45,8 @@ NPanel { return numWeeks * rowHeight } - property real contentPreferredHeight: banner.height + calendar.height + weatherLoader.height + Style.marginM * 4 + (Settings.data.location.weatherEnabled && Settings.data.location.showCalendarWeather) * Style.marginM + // Use implicitHeight from content + margins to avoid binding loops + property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2 ColumnLayout { id: content @@ -65,20 +67,6 @@ NPanel { isCurrentMonth = checkIsCurrentMonth() } - Shortcut { - sequence: "Escape" - onActivated: { - if (timerActive) { - cancelTimer() - } else { - cancelTimer() - root.close() - } - } - context: Qt.WidgetShortcut - enabled: root.opened - } - Connections { target: Time function onDateChanged() { @@ -623,7 +611,7 @@ NPanel { onClicked: { const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}` Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]) - PanelService.getPanel("calendarPanel").toggle(null) + root.close() } onExited: { diff --git a/Modules/Bar/Extras/BarWidgetLoader.qml b/Modules/Bar/Extras/BarWidgetLoader.qml index 37ea0391..fb90c645 100644 --- a/Modules/Bar/Extras/BarWidgetLoader.qml +++ b/Modules/Bar/Extras/BarWidgetLoader.qml @@ -6,15 +6,17 @@ import qs.Commons Item { id: root - property string widgetId: "" - property var widgetProps: ({}) - property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : "" - property string section: widgetProps && widgetProps.section || "" - property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 + required property string widgetId + required property var widgetScreen + required property var widgetProps property string barDensity: "default" readonly property real scaling: barDensity === "mini" ? 0.8 : (barDensity === "compact" ? 0.9 : 1.0) + // Extract section info from widgetProps + readonly property string section: widgetProps.section || "" + readonly property int sectionIndex: widgetProps.sectionWidgetIndex || 0 + // Don't reserve space unless the loaded widget is really visible implicitWidth: getImplicitSize(loader.item, "implicitWidth") implicitHeight: getImplicitSize(loader.item, "implicitHeight") @@ -26,56 +28,54 @@ Item { Loader { id: loader anchors.fill: parent - active: widgetId !== "" asynchronous: false - sourceComponent: { - if (!active) { - return null - } - return BarWidgetRegistry.getWidget(widgetId) - } + sourceComponent: BarWidgetRegistry.getWidget(widgetId) onLoaded: { - if (item && widgetProps) { - // Apply properties to loaded widget - for (var prop in widgetProps) { - if (item.hasOwnProperty(prop)) { - item[prop] = widgetProps[prop] - } - } - // Explicitly set scaling property - if (item.hasOwnProperty("scaling")) { - item.scaling = Qt.binding(function () { - return root.scaling - }) + if (!item) + return + + Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name) + + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] } } + // Set screen property + if (item.hasOwnProperty("screen")) { + item.screen = widgetScreen + } + + // Set scaling property + if (item.hasOwnProperty("scaling")) { + item.scaling = Qt.binding(function () { + return root.scaling + }) + } + // Register this widget instance with BarService - if (screenName && section) { - BarService.registerWidget(screenName, section, widgetId, sectionIndex, item) - } + BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item) + // Call custom onLoaded if it exists if (item.hasOwnProperty("onLoaded")) { item.onLoaded() } - - //Logger.i("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) } Component.onDestruction: { // Unregister when destroyed - if (screenName && section) { - BarService.unregisterWidget(screenName, section, widgetId, sectionIndex) + if (widgetScreen && section) { + BarService.unregisterWidget(widgetScreen.name, section, widgetId, sectionIndex) } - // Explicitly clear references - widgetProps = null } } // Error handling - onWidgetIdChanged: { - if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) { + Component.onCompleted: { + if (!BarWidgetRegistry.hasWidget(widgetId)) { Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId) } } diff --git a/Modules/Bar/WiFi/WiFiPanel.qml b/Modules/Bar/WiFi/WiFiPanel.qml index 281ace59..29b09685 100644 --- a/Modules/Bar/WiFi/WiFiPanel.qml +++ b/Modules/Bar/WiFi/WiFiPanel.qml @@ -12,7 +12,6 @@ NPanel { preferredWidth: 420 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true property string passwordSsid: "" property string passwordInput: "" diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index e19e564f..7eb2a714 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -94,7 +94,7 @@ Item { autoHide: false forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) - onClicked: PanelService.getPanel("batteryPanel")?.toggle(this) + onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this) tooltipText: { let lines = [] if (testMode) { diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index 1c3fd2b0..84ca7f41 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -54,8 +54,8 @@ Item { autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) tooltipText: { if (pill.text !== "") { return pill.text diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 5246a182..ca3c7879 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -108,13 +108,13 @@ Item { } onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 02e8b6d6..3b7a2208 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -121,7 +121,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: { - if (!PanelService.getPanel("calendarPanel")?.active) { + if (!PanelService.getPanel("calendarPanel", screen)?.active) { TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection()) } } @@ -130,7 +130,7 @@ Rectangle { } onClicked: { TooltipService.hide() - PanelService.getPanel("calendarPanel")?.toggle(this) + PanelService.getPanel("calendarPanel", screen)?.toggle(this) } } } diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index 9e08fa84..d6950a35 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -44,8 +44,8 @@ NIconButton { colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mHover colorBorder: Color.transparent colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent - onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() + onClicked: PanelService.getPanel("controlCenterPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle() IconImage { id: customOrDistroLogo diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 9c501aef..222b0824 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -184,7 +184,7 @@ Item { Logger.i("CustomButton", `Executing command: ${leftClickExec}`) } else if (!hasExec) { // No script was defined, open settings - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Bar settingsPanel.open() } diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 4592653d..f0a8cb3b 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -105,7 +105,7 @@ Item { } } onClicked: { - PanelService.getPanel("audioPanel")?.toggle(this) + PanelService.getPanel("audioPanel", screen)?.toggle(this) } onRightClicked: { AudioService.setInputMuted(!AudioService.inputMuted) diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 80d2c01d..06580d4e 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -43,7 +43,7 @@ NIconButton { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index ccceeaa3..55f41d9b 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -56,7 +56,7 @@ NIconButton { colorBorderHover: Color.transparent onClicked: { - var panel = PanelService.getPanel("notificationHistoryPanel") + var panel = PanelService.getPanel("notificationHistoryPanel", screen) panel?.toggle(this) } diff --git a/Modules/Bar/Widgets/SessionMenu.qml b/Modules/Bar/Widgets/SessionMenu.qml index bb24f446..d1b79998 100644 --- a/Modules/Bar/Widgets/SessionMenu.qml +++ b/Modules/Bar/Widgets/SessionMenu.qml @@ -20,5 +20,5 @@ NIconButton { colorFg: Color.mError colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle() + onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle() } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 07e7b755..f8f61375 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -279,7 +279,6 @@ Rectangle { function open() { visible = true - PanelService.willOpenPanel(trayPanel) } function close() { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index d494d3cb..0c603c6f 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -90,7 +90,7 @@ Item { } } onClicked: { - PanelService.getPanel("audioPanel")?.toggle(this) + PanelService.getPanel("audioPanel", screen)?.toggle(this) } onRightClicked: { AudioService.setOutputMuted(!AudioService.muted) diff --git a/Modules/Bar/Widgets/WallpaperSelector.qml b/Modules/Bar/Widgets/WallpaperSelector.qml index e48f368a..322b54c0 100644 --- a/Modules/Bar/Widgets/WallpaperSelector.qml +++ b/Modules/Bar/Widgets/WallpaperSelector.qml @@ -20,5 +20,5 @@ NIconButton { colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) + onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle(this) } diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 25de98ae..19f37ed9 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -76,8 +76,8 @@ Item { autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) tooltipText: { if (pill.text !== "") { return pill.text diff --git a/Modules/ControlCenter/Cards/ProfileCard.qml b/Modules/ControlCenter/Cards/ProfileCard.qml index 1eaffb3a..408c3f19 100644 --- a/Modules/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/ControlCenter/Cards/ProfileCard.qml @@ -60,8 +60,9 @@ NBox { icon: "settings" tooltipText: I18n.tr("tooltips.open-settings") onClicked: { - settingsPanel.requestedTab = SettingsPanel.Tab.General - settingsPanel.open() + var panel = PanelService.getPanel("settingsPanel", screen) + panel.requestedTab = SettingsPanel.Tab.General + panel.open() } } @@ -69,8 +70,8 @@ NBox { icon: "power" tooltipText: I18n.tr("tooltips.session-menu") onClicked: { - sessionMenuPanel.open() - controlCenterPanel.close() + PanelService.getPanel("sessionMenuPanel", screen)?.open() + PanelService.getPanel("controlCenterPanel", screen)?.close() } } @@ -78,7 +79,7 @@ NBox { icon: "close" tooltipText: I18n.tr("tooltips.close") onClicked: { - controlCenterPanel.close() + PanelService.getPanel("controlCenterPanel", screen)?.close() } } } diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/ControlCenter/Cards/ShortcutsCard.qml index 2100a76b..18e593d8 100644 --- a/Modules/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/ControlCenter/Cards/ShortcutsCard.qml @@ -28,10 +28,13 @@ RowLayout { Repeater { model: Settings.data.controlCenter.shortcuts.left delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + Layout.fillWidth: false widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen widgetProps: { - "screen": root.modelData || null, "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, @@ -63,10 +66,13 @@ RowLayout { Repeater { model: Settings.data.controlCenter.shortcuts.right delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + Layout.fillWidth: false widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen widgetProps: { - "screen": root.modelData || null, "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index 0e899019..10cccf70 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -10,7 +10,6 @@ import qs.Widgets NPanel { id: root - panelKeyboardFocus: true preferredWidth: Math.round(460 * Style.uiScaleRatio) preferredHeight: { var height = 0 diff --git a/Modules/ControlCenter/ControlCenterWidgetLoader.qml b/Modules/ControlCenter/ControlCenterWidgetLoader.qml index e0360d4c..ebfb05f7 100644 --- a/Modules/ControlCenter/ControlCenterWidgetLoader.qml +++ b/Modules/ControlCenter/ControlCenterWidgetLoader.qml @@ -6,9 +6,10 @@ import qs.Commons Item { id: root - property string widgetId: "" - property var widgetProps: ({}) - property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : "" + required property string widgetId + required property var widgetScreen + required property var widgetProps + property string section: widgetProps && widgetProps.section || "" property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 @@ -23,30 +24,29 @@ Item { Loader { id: loader anchors.fill: parent - active: widgetId !== "" asynchronous: false - sourceComponent: { - if (!active) { - return null - } - return ControlCenterWidgetRegistry.getWidget(widgetId) - } + sourceComponent: ControlCenterWidgetRegistry.getWidget(widgetId) onLoaded: { - if (item && widgetProps) { - // Apply properties to loaded widget - for (var prop in widgetProps) { - if (item.hasOwnProperty(prop)) { - item[prop] = widgetProps[prop] - } + if (!item) + return + + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] } } + // Set screen property + if (item.hasOwnProperty("screen")) { + item.screen = widgetScreen + } + + // Call custom onLoaded if it exists if (item.hasOwnProperty("onLoaded")) { item.onLoaded() } - - //Logger.i("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) } Component.onDestruction: { @@ -56,8 +56,8 @@ Item { } // Error handling - onWidgetIdChanged: { - if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) { + Component.onCompleted: { + if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) { Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId) } } diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/ControlCenter/Widgets/Bluetooth.qml index e4549a31..5378156d 100644 --- a/Modules/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/ControlCenter/Widgets/Bluetooth.qml @@ -9,5 +9,7 @@ NIconButtonHot { icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" tooltipText: I18n.tr("quickSettings.bluetooth.tooltip.action") - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onClicked: { + PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) + } } diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/ControlCenter/Widgets/NightLight.qml index eb338882..930a8f96 100644 --- a/Modules/ControlCenter/Widgets/NightLight.qml +++ b/Modules/ControlCenter/Widgets/NightLight.qml @@ -25,7 +25,7 @@ NIconButtonHot { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/ControlCenter/Widgets/Notifications.qml b/Modules/ControlCenter/Widgets/Notifications.qml index ae291517..35f5cdf7 100644 --- a/Modules/ControlCenter/Widgets/Notifications.qml +++ b/Modules/ControlCenter/Widgets/Notifications.qml @@ -10,6 +10,6 @@ NIconButtonHot { icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" hot: Settings.data.notifications.doNotDisturb tooltipText: I18n.tr("quickSettings.notifications.tooltip.action") - onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(this) + onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this) onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/ControlCenter/Widgets/PowerProfile.qml index d26922ed..ca5f497f 100644 --- a/Modules/ControlCenter/Widgets/PowerProfile.qml +++ b/Modules/ControlCenter/Widgets/PowerProfile.qml @@ -14,7 +14,7 @@ NIconButtonHot { enabled: hasPP icon: PowerProfileService.getIcon() hot: !PowerProfileService.isDefault() - tooltipText: hasPP ? I18n.tr("quickSettings.powerProfile.tooltip.action") : I18n.tr("quickSettings.powerProfile.tooltip.disabled") + tooltipText: I18n.tr("quickSettings.powerProfile.tooltip.action") onClicked: { PowerProfileService.cycleProfile() } diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/ControlCenter/Widgets/ScreenRecorder.qml index 7a58c8f3..85fbacd9 100644 --- a/Modules/ControlCenter/Widgets/ScreenRecorder.qml +++ b/Modules/ControlCenter/Widgets/ScreenRecorder.qml @@ -14,7 +14,7 @@ NIconButtonHot { onClicked: { ScreenRecorderService.toggleRecording() if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("controlCenterPanel") + var panel = PanelService.getPanel("controlCenterPanel", screen) panel?.close() } } diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/ControlCenter/Widgets/WallpaperSelector.qml index a725974c..daa941ee 100644 --- a/Modules/ControlCenter/Widgets/WallpaperSelector.qml +++ b/Modules/ControlCenter/Widgets/WallpaperSelector.qml @@ -10,6 +10,6 @@ NIconButtonHot { enabled: Settings.data.wallpaper.enabled icon: "wallpaper-selector" tooltipText: I18n.tr("quickSettings.wallpaperSelector.tooltip.action") - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle() + onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle() onRightClicked: WallpaperService.setRandomWallpaper() } diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml index eea64700..8970dc45 100644 --- a/Modules/ControlCenter/Widgets/WiFi.qml +++ b/Modules/ControlCenter/Widgets/WiFi.qml @@ -29,5 +29,5 @@ NIconButtonHot { } tooltipText: I18n.tr("quickSettings.wifi.tooltip.action") - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 03c6f0af..5f1ed23b 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -16,8 +16,8 @@ NPanel { preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 - panelKeyboardFocus: true panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) + panelKeyboardFocus: true // Needs Exclusive focus for text input // Positioning readonly property string launcherPosition: Settings.data.appLauncher.position @@ -40,6 +40,51 @@ NPanel { readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6) readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) + // Override keyboard handlers from NPanel for navigation + function onTabPressed() { + selectNextWrapped() + } + + function onShiftTabPressed() { + selectPreviousWrapped() + } + + function onUpPressed() { + selectPreviousWrapped() + } + + function onDownPressed() { + selectNextWrapped() + } + + function onReturnPressed() { + activate() + } + + function onHomePressed() { + selectFirst() + } + + function onEndPressed() { + selectLast() + } + + function onPageUpPressed() { + selectPreviousPage() + } + + function onPageDownPressed() { + selectNextPage() + } + + function onCtrlJPressed() { + selectNextWrapped() + } + + function onCtrlKPressed() { + selectPreviousWrapped() + } + // Public API for plugins function setSearchText(text) { searchText = text @@ -151,6 +196,54 @@ NPanel { } } + // Navigation functions + function selectNextWrapped() { + if (results.length > 0) { + selectedIndex = (selectedIndex + 1) % results.length + } + } + + function selectPreviousWrapped() { + if (results.length > 0) { + selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length + } + } + + function selectFirst() { + selectedIndex = 0 + } + + function selectLast() { + if (results.length > 0) { + selectedIndex = results.length - 1 + } else { + selectedIndex = 0 + } + } + + function selectNextPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height + selectedIndex = Math.min(selectedIndex + page, results.length - 1) + } + } + + function selectPreviousPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height + selectedIndex = Math.max(selectedIndex - page, 0) + } + } + + function activate() { + if (results.length > 0 && results[selectedIndex]) { + const item = results[selectedIndex] + if (item.onActivate) { + item.onActivate() + } + } + } + // UI panelContent: Rectangle { id: ui @@ -198,6 +291,19 @@ NPanel { } } + // Focus management + Connections { + target: root + function onOpened() { + // Delay focus to ensure window has keyboard focus + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + Behavior on opacity { NumberAnimation { duration: Style.animationFast @@ -205,102 +311,6 @@ NPanel { } } - // --------------------- - // Navigation - function selectNextWrapped() { - if (results.length > 0) { - selectedIndex = (selectedIndex + 1) % results.length - } - } - - function selectPreviousWrapped() { - if (results.length > 0) { - selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length - } - } - - function selectFirst() { - selectedIndex = 0 - } - - function selectLast() { - if (results.length > 0) { - selectedIndex = results.length - 1 - } else { - selectedIndex = 0 - } - } - - function selectNextPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(resultsList.height / entryHeight)) - selectedIndex = Math.min(selectedIndex + page, results.length - 1) - } - } - function selectPreviousPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(resultsList.height / entryHeight)) - selectedIndex = Math.max(selectedIndex - page, 0) - } - } - - function activate() { - if (results.length > 0 && results[selectedIndex]) { - const item = results[selectedIndex] - if (item.onActivate) { - item.onActivate() - } - } - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: ui.selectNextWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Tab" - onActivated: ui.selectNextWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "PgDown" // or "PageDown" - onActivated: ui.selectNextPage() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "PgUp" // or "PageUp" - onActivated: ui.selectPreviousPage() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Home" - onActivated: ui.selectFirst() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "End" - onActivated: ui.selectLast() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - ColumnLayout { anchors.fill: parent anchors.margins: Style.marginL @@ -319,36 +329,8 @@ NPanel { onTextChanged: searchText = text Component.onCompleted: { - if (searchInput.inputItem && searchInput.inputItem.visible) { + if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus() - - // Override the TextField's default Home/End behavior - searchInput.inputItem.Keys.priority = Keys.BeforeItem - searchInput.inputItem.Keys.onPressed.connect(function (event) { - // Intercept Home, End, and Numpad Enter BEFORE the TextField handles them - if (event.key === Qt.Key_Home) { - ui.selectFirst() - event.accepted = true - return - } else if (event.key === Qt.Key_End) { - ui.selectLast() - event.accepted = true - return - } else if (event.key === Qt.Key_Enter) { - ui.activate() - event.accepted = true - return - } - }) - searchInput.inputItem.Keys.onDownPressed.connect(function (event) { - ui.selectNextWrapped() - }) - searchInput.inputItem.Keys.onUpPressed.connect(function (event) { - ui.selectPreviousWrapped() - }) - searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { - ui.activate() - }) } } } @@ -588,7 +570,7 @@ NPanel { onClicked: mouse => { if (mouse.button === Qt.LeftButton) { selectedIndex = index - ui.activate() + root.activate() mouse.accepted = true } } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index f2336910..3d7c61b1 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -12,9 +12,8 @@ import qs.Widgets NPanel { id: root - preferredWidth: 380 - preferredHeight: 480 - panelKeyboardFocus: true + preferredWidth: 380 * Style.uiScaleRatio + preferredHeight: 480 * Style.uiScaleRatio onOpened: function () { NotificationService.updateLastSeenTs() diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml index 623a93c5..6d789d6e 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -15,9 +15,9 @@ NPanel { preferredWidth: 400 * Style.uiScaleRatio preferredHeight: 340 * Style.uiScaleRatio + panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true // Timer properties property int timerDuration: 9000 // 9 seconds @@ -148,6 +148,52 @@ NPanel { } } + // Override keyboard handlers from NPanel + function onEscapePressed() { + if (timerActive) { + cancelTimer() + } else { + cancelTimer() + close() + } + } + + function onTabPressed() { + selectNextWrapped() + } + + function onShiftTabPressed() { + selectPreviousWrapped() + } + + function onUpPressed() { + selectPreviousWrapped() + } + + function onDownPressed() { + selectNextWrapped() + } + + function onReturnPressed() { + activate() + } + + function onHomePressed() { + selectFirst() + } + + function onEndPressed() { + selectLast() + } + + function onCtrlJPressed() { + selectNextWrapped() + } + + function onCtrlKPressed() { + selectPreviousWrapped() + } + // Countdown timer Timer { id: countdownTimer @@ -165,81 +211,6 @@ NPanel { id: ui color: Color.transparent - // Keyboard shortcuts - Shortcut { - sequence: "Ctrl+K" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Down" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Tab" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Home" - onActivated: ui.selectFirst() - enabled: root.opened - } - - Shortcut { - sequence: "End" - onActivated: ui.selectLast() - enabled: root.opened - } - - Shortcut { - sequence: "Return" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Enter" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Escape" - onActivated: { - if (timerActive) { - cancelTimer() - } else { - cancelTimer() - root.close() - } - } - context: Qt.WidgetShortcut - enabled: root.opened - } - // Navigation functions function selectFirst() { root.selectFirst() diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index a8fc5bce..08b4c484 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -16,9 +16,6 @@ NPanel { panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true - - draggable: !PanelService.hasOpenedPopup // Tabs enumeration, order is NOT relevant enum Tab { @@ -287,6 +284,39 @@ NPanel { } } + // Override keyboard handlers from NPanel + function onTabPressed() { + selectNextTab() + } + + function onShiftTabPressed() { + selectPreviousTab() + } + + function onUpPressed() { + scrollUp() + } + + function onDownPressed() { + scrollDown() + } + + function onPageUpPressed() { + scrollPageUp() + } + + function onPageDownPressed() { + scrollPageDown() + } + + function onCtrlJPressed() { + scrollDown() + } + + function onCtrlKPressed() { + scrollUp() + } + panelContent: Rectangle { color: Color.transparent @@ -296,62 +326,6 @@ NPanel { anchors.margins: Style.marginL spacing: 0 - // Keyboard shortcuts container - Item { - Layout.preferredWidth: 0 - Layout.preferredHeight: 0 - - // Scrolling via keyboard - Shortcut { - sequence: "Down" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "PgDown" - onActivated: root.scrollPageDown() - enabled: root.opened - } - - Shortcut { - sequence: "PgUp" - onActivated: root.scrollPageUp() - enabled: root.opened - } - - // Changing tab via keyboard - Shortcut { - sequence: "Tab" - onActivated: root.selectNextTab() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: root.selectPreviousTab() - enabled: root.opened - } - } - // Main content area RowLayout { Layout.fillWidth: true diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index a9552659..639010e5 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -25,7 +25,7 @@ ColumnLayout { // Handler for drag start - disables panel background clicks function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.disableBackgroundClick) { panel.disableBackgroundClick() } @@ -33,7 +33,7 @@ ColumnLayout { // Handler for drag end - re-enables panel background clicks function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.enableBackgroundClick) { panel.enableBackgroundClick() } @@ -102,6 +102,14 @@ ColumnLayout { onToggled: checked => Settings.data.bar.floating = checked } + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.bar.appearance.outer-corners.label") + description: I18n.tr("settings.bar.appearance.outer-corners.description") + checked: Settings.data.bar.outerCorners + onToggled: checked => Settings.data.bar.outerCorners = checked + } + // Floating bar options - only show when floating is enabled ColumnLayout { visible: Settings.data.bar.floating diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 372add57..7b1a91a7 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -40,7 +40,7 @@ ColumnLayout { // Handler for drag start - disables panel background clicks function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.disableBackgroundClick) { panel.disableBackgroundClick() } @@ -48,7 +48,7 @@ ColumnLayout { // Handler for drag end - re-enables panel background clicks function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.enableBackgroundClick) { panel.enableBackgroundClick() } diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml index 86cf97ee..dd6cee27 100644 --- a/Modules/Settings/Tabs/LauncherTab.qml +++ b/Modules/Settings/Tabs/LauncherTab.qml @@ -22,6 +22,9 @@ ColumnLayout { model: [{ "key": "center", "name": I18n.tr("options.launcher.position.center") + }, { + "key": "top_center", + "name": I18n.tr("options.launcher.position.top_center") }, { "key": "top_left", "name": I18n.tr("options.launcher.position.top_left") @@ -37,9 +40,6 @@ ColumnLayout { }, { "key": "bottom_center", "name": I18n.tr("options.launcher.position.bottom_center") - }, { - "key": "top_center", - "name": I18n.tr("options.launcher.position.top_center") }] currentKey: Settings.data.appLauncher.position onSelected: function (key) { diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index 063714e5..a466c7ec 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -26,6 +26,13 @@ ColumnLayout { onToggled: checked => Settings.data.ui.tooltipsEnabled = checked } + NToggle { + label: I18n.tr("settings.user-interface.dim-desktop.label") + description: I18n.tr("settings.user-interface.dim-desktop.description") + checked: Settings.data.general.dimDesktop + onToggled: checked => Settings.data.general.dimDesktop = checked + } + NToggle { label: I18n.tr("settings.user-interface.panels-attached-to-bar.label") description: I18n.tr("settings.user-interface.panels-attached-to-bar.description") diff --git a/Modules/SetupWizard/SetupWizard.qml b/Modules/SetupWizard/SetupWizard.qml index 5be6df91..996d7d57 100644 --- a/Modules/SetupWizard/SetupWizard.qml +++ b/Modules/SetupWizard/SetupWizard.qml @@ -14,17 +14,17 @@ NPanel { preferredHeight: 600 * Style.uiScaleRatio preferredWidthRatio: 0.4 preferredHeightRatio: 0.6 + panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true - - // Prevent closing during setup - backgroundClickEnabled: false - draggable: false property int currentStep: 0 property int totalSteps: 5 + // Override Escape handler to prevent closing the setup wizard + function onEscapePressed() {// Do nothing - prevent ESC from closing the setup wizard + } + // Setup wizard data property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory property string selectedWallpaper: "" @@ -42,17 +42,6 @@ NPanel { anchors.margins: Style.marginXL spacing: Style.marginL - // Override ESC key to prevent closing during setup - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: { - - // Do nothing - prevent ESC from closing the setup wizard - } - context: Qt.WindowShortcut - } - // Step content - takes most of the space Item { Layout.fillWidth: true diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml index 2a1f6d57..472cdfcc 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -12,15 +12,87 @@ import "../../Helpers/FuzzySort.js" as FuzzySort NPanel { id: root - preferredWidth: 640 * Style.uiScaleRatio - preferredHeight: 480 * Style.uiScaleRatio - preferredWidthRatio: 0.4 + preferredWidth: 800 * Style.uiScaleRatio + preferredHeight: 600 * Style.uiScaleRatio + preferredWidthRatio: 0.5 preferredHeightRatio: 0.52 - panelAnchorHorizontalCenter: true - panelAnchorVerticalCenter: true - panelKeyboardFocus: true - draggable: !PanelService.hasOpenedPopup + // Positioning - Use launcher position. This saves a setting... + readonly property string launcherPosition: Settings.data.appLauncher.position + panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") + panelAnchorVerticalCenter: launcherPosition === "center" + panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") + panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") + panelAnchorBottom: launcherPosition.startsWith("bottom_") + panelAnchorTop: launcherPosition.startsWith("top_") + + // panelAnchorHorizontalCenter: true + // panelAnchorVerticalCenter: true + panelKeyboardFocus: true // Needs Exclusive focus for text input (search) + + // Store direct reference to content for instant access + property var contentItem: null + + // Override keyboard handlers to enable grid navigation + function onDownPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView) { + if (!view.gridView.activeFocus) { + view.gridView.forceActiveFocus() + if (view.gridView.currentIndex < 0) { + view.gridView.currentIndex = 0 + } + } else { + view.gridView.moveCurrentIndexDown() + } + } + } + + function onUpPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexUp() + } + } + + function onLeftPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexLeft() + } + } + + function onRightPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexRight() + } + } + + function onReturnPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + let gridView = view.gridView + if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.length) { + let path = gridView.model[gridView.currentIndex] + if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { + WallpaperService.changeWallpaper(path, undefined) + } else { + WallpaperService.changeWallpaper(path, view.targetScreen.name) + } + } + } + } panelContent: Rectangle { id: wallpaperPanel @@ -37,9 +109,31 @@ NPanel { } property var currentScreen: Quickshell.screens[currentScreenIndex] property string filterText: "" + property alias screenRepeater: screenRepeater + + Component.onCompleted: { + root.contentItem = wallpaperPanel + } color: Color.transparent + // Focus management + Connections { + target: root + function onOpened() { + // Ensure contentItem is set + if (!root.contentItem) { + root.contentItem = wallpaperPanel + } + // Give initial focus to search input + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + // Debounce timer for search Timer { id: searchDebounceTimer @@ -85,7 +179,7 @@ NPanel { tooltipText: I18n.tr("settings.wallpaper.settings.section.label") baseSize: Style.baseWidgetSize * 0.8 onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper settingsPanel.open() } @@ -324,7 +418,7 @@ NPanel { model: filteredWallpapers - property int columns: 4 + property int columns: 5 property int itemSize: cellWidth cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index 3f942403..14b831c9 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -62,7 +62,7 @@ Singleton { } else { BatteryService.initialSetter = true ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup")) - PanelService.getPanel("batteryPanel")?.toggle(this) + PanelService.getPanel("batteryPanel", screen)?.toggle(this) uninstallerProcess.running = true } } @@ -123,7 +123,7 @@ Singleton { Settings.data.battery.chargingMode = BatteryService.chargingMode } else if (exitCode === 2) { ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup")) - PanelService.getPanel("batteryPanel")?.toggle(this) + PanelService.getPanel("batteryPanel", screen)?.toggle(this) BatteryService.runInstaller() } else { ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed")) diff --git a/Services/CavaService.qml b/Services/CavaService.qml index 30fccea1..cf5c174c 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -8,7 +8,15 @@ import qs.Commons Singleton { id: root - property bool shouldRun: BarService.hasAudioVisualizer || (PanelService.getPanel("controlCenterPanel") === PanelService.openedPanel) || PanelService.lockScreen.active + + /** + * Cava runs if: + * - Bar has an audio visualizer + * - LockScreen is opened + * - A control center is open + */ + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + property var values: Array(barsCount).fill(0) property int barsCount: 48 property var config: ({ diff --git a/Services/IPCService.qml b/Services/IPCService.qml index 7a569485..a9323518 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland +import Quickshell.Widgets import qs.Commons import qs.Services @@ -27,7 +28,10 @@ Item { IpcHandler { target: "settings" function toggle() { - settingsPanel.toggle() + root.withTargetScreen(screen => { + var settingsPanel = PanelService.getPanel("settingsPanel", screen) + settingsPanel.toggle() + }) } } @@ -35,7 +39,10 @@ Item { target: "notifications" function toggleHistory() { // Will attempt to open the panel next to the bar button if any. - notificationHistoryPanel.toggle(null, "NotificationHistory") + root.withTargetScreen(screen => { + var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen) + notificationHistoryPanel.toggle(null, "NotificationHistory") + }) } function toggleDND() { Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb @@ -63,15 +70,24 @@ Item { IpcHandler { target: "launcher" function toggle() { - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.toggle() + }) } function clipboard() { - launcherPanel.setSearchText(">clip ") - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.setSearchText(">clip ") + launcherPanel.toggle() + }) } function calculator() { - launcherPanel.setSearchText(">calc ") - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.setSearchText(">calc ") + launcherPanel.toggle() + }) } } @@ -158,7 +174,10 @@ Item { IpcHandler { target: "sessionMenu" function toggle() { - sessionMenuPanel.toggle() + root.withTargetScreen(screen => { + var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen) + sessionMenuPanel.toggle() + }) } function lockAndSuspend() { @@ -170,7 +189,10 @@ Item { target: "controlCenter" function toggle() { // Will attempt to open the panel next to the bar button if any. - controlCenterPanel.toggle(null, "ControlCenter") + root.withTargetScreen(screen => { + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen) + controlCenterPanel.toggle(null, "ControlCenter") + }) } } @@ -179,7 +201,10 @@ Item { target: "wallpaper" function toggle() { if (Settings.data.wallpaper.enabled) { - wallpaperPanel.toggle() + root.withTargetScreen(screen => { + var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen) + wallpaperPanel.toggle() + }) } } @@ -228,6 +253,7 @@ Item { } } } + IpcHandler { target: "powerProfile" function cycle() { @@ -248,6 +274,7 @@ Item { } } } + IpcHandler { target: "media" function playPause() { @@ -292,4 +319,83 @@ Item { MediaService.seekByRatio(positionVal) } } + + // Queue an IPC panel operation - will execute when screen is detected + function withTargetScreen(callback) { + if (pendingCallback) { + Logger.w("IPC", "Another IPC call is pending, ignoring new call") + return + } + + // Single monitor setup can execute immediately + if (Quickshell.screens.length === 1) { + pendingCallback(Quickshell.screens[0]) + } else { + // Multi-monitors setup needs to start async detection + detectedScreen = null + pendingCallback = callback + screenDetectorLoader.active = true + } + } + + + /** + * For IPC calls on multi-monitors setup that will open panels on screen, + * we need to open a QS PanelWindow and wait for it's "screen" property to stabilize. + */ + property ShellScreen detectedScreen: null + property var pendingCallback: null + + Timer { + id: screenDetectorDebounce + running: false + interval: 20 + onTriggered: { + Logger.d("IPC", "Screen debounced to:", detectedScreen?.name || "null") + + // Execute pending callback if any + if (pendingCallback) { + // Verify we have a NFullScreenWindow for this screen + var monitors = Settings.data.bar.monitors || [] + if (!(monitors.length === 0 || monitors.includes(detectedScreen.name))) { + // Fall back to first enabled screen as we can NOT show a panel on a screen without a Bar/NFullScreenWindow + if (monitors.length === 0 && Quickshell.screens.length > 0) { + detectedScreen = Quickshell.screens[0] + } else { + for (var i = 0; i < Quickshell.screens.length; i++) { + if (monitors.includes(Quickshell.screens[i].name)) { + detectedScreen = Quickshell.screens[i] + break + } + } + } + } + Logger.d("IPC", "Executing pending IPC callback on screen:", detectedScreen.name) + pendingCallback(detectedScreen) + pendingCallback = null + } + + // Clean up + screenDetectorLoader.active = false + } + } + + // Invisible dummy PanelWindow to detect which screen should receive IPC calls + Loader { + id: screenDetectorLoader + active: false + + sourceComponent: PanelWindow { + implicitWidth: 0 + implicitHeight: 0 + color: Color.transparent + WlrLayershell.exclusionMode: ExclusionMode.Ignore + mask: Region {} + + onScreenChanged: { + detectedScreen = screen + screenDetectorDebounce.restart() + } + } + } } diff --git a/Services/MediaService.qml b/Services/MediaService.qml index 505a945b..b96cafbd 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -304,7 +304,7 @@ Singleton { repeat: true running: true onTriggered: { - Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) + //Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) if (autoSwitchingPaused) return // Only update if we don't have a playing player or if current player is paused diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 1ee055c1..6c2b071b 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -14,6 +14,7 @@ Singleton { property var registeredPanels: ({}) property var openedPanel: null signal willOpen + signal didClose // Currently opened popups, can have more than one. // ex: when opening an NIconPicker from a widget setting. @@ -21,15 +22,53 @@ Singleton { property bool hasOpenedPopup: false signal popupChanged - // Register this panel - function registerPanel(panel) { - registeredPanels[panel.objectName] = panel - Logger.d("PanelService", "Registered:", panel.objectName) + // Registered panel loaders (before they're loaded) + property var registeredPanelLoaders: ({}) + + // Register a panel loader (called before panel is loaded) + function registerPanelLoader(panelLoader, objectName) { + registeredPanelLoaders[objectName] = panelLoader + Logger.d("PanelService", "Registered panel loader:", objectName) } - // Returns a panel - function getPanel(name) { - return registeredPanels[name] || null + // Register this panel (called after panel is loaded) + function registerPanel(panel) { + registeredPanels[panel.objectName] = panel + Logger.i("PanelService", "Registered panel:", panel.objectName) + } + + // Returns a panel (loads it on-demand if not yet loaded) + function getPanel(name, screen) { + if (!screen) { + Logger.w("PanelService", "missing screen for getPanel:", name) + Logger.callStack() + // If no screen specified, return the first matching panel + for (var key in registeredPanels) { + if (key.startsWith(name + "-")) { + return registeredPanels[key] + } + } + return null + } + + var panelKey = `${name}-${screen.name}` + + // Check if panel is already loaded + if (registeredPanels[panelKey]) { + return registeredPanels[panelKey] + } + + // Panel not loaded yet - try to load it via the loader + if (registeredPanelLoaders[panelKey]) { + Logger.d("PanelService", "Loading panel on-demand:", panelKey) + registeredPanelLoaders[panelKey].ensureLoaded() + // After ensureLoaded(), the panel should register itself via registerPanel() + // Return it if it registered synchronously + return registeredPanels[panelKey] || null + } + + Logger.w("PanelService", "Panel not found:", panelKey) + return null } // Check if a panel exists @@ -52,6 +91,9 @@ Singleton { if (openedPanel && openedPanel === panel) { openedPanel = null } + + // emit signal + didClose() } // Popups diff --git a/Widgets/BarExclusionZone.qml b/Widgets/BarExclusionZone.qml new file mode 100644 index 00000000..a2448a92 --- /dev/null +++ b/Widgets/BarExclusionZone.qml @@ -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) + } +} diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml new file mode 100644 index 00000000..e5302156 --- /dev/null +++ b/Widgets/NFullScreenWindow.qml @@ -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() + } + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 98dfb453..2f301f53 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -1,35 +1,39 @@ import QtQuick -import QtQuick.Effects import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -Loader { + +/** + * NPanel for use within NFullScreenWindow + */ +Item { id: root - property ShellScreen screen + // Screen property provided by NFullScreenWindow + property ShellScreen screen: null readonly property real opacityThreshold: 0.33 - property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold) - property bool useOverlay: Settings.data.ui.panelsOverlayLayer + property bool forceDetached: false // Force panel to be detached regardless of settings + property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold && !forceDetached) + + // Keyboard focus documentation (not currently used for focus mode) + // Just for documentation: true for panels with text input + // NFullScreenWindow always uses Exclusive focus when any panel is open + property bool panelKeyboardFocus: false property Component panelContent: null - // Panel size properties. Can be set directly on NPanel, or dynamically by the content. - // For dynamic sizing, the content should expose contentPreferredWidth, contentPreferredHeight, - // contentPreferredWidthRatio, or contentPreferredHeightRatio properties. - // Changes to these properties will be animated smoothly (except during panel dragging). + // Panel size properties property real preferredWidth: 700 property real preferredHeight: 900 property real preferredWidthRatio property real preferredHeightRatio property color panelBackgroundColor: Color.mSurface property color panelBorderColor: Color.mOutline - property bool draggable: false property var buttonItem: null - property string buttonName: "" + // Anchoring properties property bool panelAnchorHorizontalCenter: false property bool panelAnchorVerticalCenter: false property bool panelAnchorTop: false @@ -37,606 +41,469 @@ Loader { property bool panelAnchorLeft: false property bool panelAnchorRight: false - // Properties to support positioning relative to the opener (button) + // Button position properties property bool useButtonPosition: false property point buttonPosition: Qt.point(0, 0) property int buttonWidth: 0 property int buttonHeight: 0 - property bool panelKeyboardFocus: false - property bool backgroundClickEnabled: true + // Track whether panel is open + property bool isPanelOpen: false // Animation properties - property real panelBackgroundOpacity: 0 - property real panelContentOpacity: 0 - property real dimmingOpacity: 0 + property real animationProgress: 0 + + // Keyboard event handlers - override these in specific panels to handle shortcuts + // These are called from NFullScreenWindow's centralized shortcuts + function onEscapePressed() { + close() + } + function onTabPressed() {} + function onShiftTabPressed() {} + function onUpPressed() {} + function onDownPressed() {} + function onLeftPressed() {} + function onRightPressed() {} + function onReturnPressed() {} + function onHomePressed() {} + function onEndPressed() {} + function onPageUpPressed() {} + function onPageDownPressed() {} + function onCtrlJPressed() {} + function onCtrlKPressed() {} + + Behavior on animationProgress { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Expose panel region for click-through mask (only when open) + readonly property var panelRegion: panelContentContainer.item?.maskRegion || null readonly property string barPosition: Settings.data.bar.position readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" - readonly property real verticalBarWidth: Style.barHeight + 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 - // Effective anchor properties - combines explicit anchors with implicit anchoring from useButtonPosition - readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") - readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") - readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") - readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") + // Helper to detect if any anchor is explicitly set + readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight + readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom + + // Effective anchor properties + // These are true when: + // 1. Explicitly anchored, OR + // 2. Using button position and bar is on that edge, OR + // 3. Attached to bar with no explicit anchors (default centering behavior) + readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") || (attachedToBar && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical) + readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (attachedToBar && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical) + readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (attachedToBar && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical) + readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (attachedToBar && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical) signal opened signal closed - active: false - asynchronous: true + // Panel visibility and sizing + visible: isPanelOpen + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 - Component.onCompleted: { - PanelService.registerPanel(root) - } - - // ----------------------------------------- - // Functions to control background click behavior - function disableBackgroundClick() { - backgroundClickEnabled = false - } - - function enableBackgroundClick() { - // Add a small delay to prevent immediate close after drag release - enableBackgroundClickTimer.restart() - } - - Timer { - id: enableBackgroundClickTimer - interval: 100 - repeat: false - onTriggered: backgroundClickEnabled = true - } - - // ----------------------------------------- + // Panel control functions function toggle(buttonItem, buttonName) { - if (!active) { + if (!isPanelOpen) { open(buttonItem, buttonName) } else { close() } } - // ----------------------------------------- function open(buttonItem, buttonName) { - root.buttonItem = buttonItem - root.buttonName = buttonName || "" + if (!buttonItem && buttonName) { + buttonItem = BarService.lookupWidget(buttonName, screen.name) + } + + if (buttonItem) { + root.buttonItem = buttonItem + // Map button position to screen coordinates + var buttonPos = buttonItem.mapToItem(null, 0, 0) + root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y) + root.buttonWidth = buttonItem.width + root.buttonHeight = buttonItem.height + root.useButtonPosition = true + } else { + // No button provided: reset button position mode + root.buttonItem = null + root.useButtonPosition = false + } setPosition() + isPanelOpen = true + animationProgress = 1 + // Notify PanelService PanelService.willOpenPanel(root) - backgroundClickEnabled = true - active = true - root.opened() + // Delay the opened signal to ensure content is fully loaded + // This ensures Component.onCompleted of the loaded content runs first + Qt.callLater(() => { + opened() + }) + + Logger.d("NPanel", "Opened panel", objectName) + Logger.d("NPanel", " Root size:", width, "x", height) } - // ----------------------------------------- function close() { - dimmingOpacity = 0 - panelBackgroundOpacity = 0 - panelContentOpacity = 0 - root.closed() - active = false - useButtonPosition = false - backgroundClickEnabled = true + isPanelOpen = false + animationProgress = 0 + + // Notify PanelService PanelService.closedPanel(root) + + closed() + + Logger.d("NPanel", "Closed panel", objectName) } - // ----------------------------------------- - function setPosition() { - // If we have a button name, we are landing here from an IPC call. - // IPC calls have no idead on which screen they panel will spawn. - // Resolve the button name to a proper button item now that we have a screen. - if (buttonName !== "" && root.screen !== null) { - buttonItem = BarService.lookupWidget(buttonName, root.screen.name) - } - - // Get the button position if provided - if (buttonItem !== undefined && buttonItem !== null) { - useButtonPosition = true - var itemPos = buttonItem.mapToItem(null, 0, 0) - buttonPosition = Qt.point(itemPos.x, itemPos.y) - buttonWidth = buttonItem.width - buttonHeight = buttonItem.height - } else { - useButtonPosition = false - } + function setPosition() {// Position calculation will be handled here + // For now, panels will be positioned based on anchors } - // ----------------------------------------- - sourceComponent: Component { - // PanelWindow has its own screen property inherited of QsWindow - PanelWindow { - id: panelWindow + // Loader for panel content + Loader { + id: panelContentContainer + anchors.fill: parent + active: root.isPanelOpen + asynchronous: false - readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) + sourceComponent: Item { + anchors.fill: parent - Component.onCompleted: { - Logger.d("NPanel", "Opened", root.objectName, "on", screen.name) - dimmingOpacity = Style.opacityHeavy - } + // Expose panelBackground for mask region + property alias maskRegion: panelBackground - Connections { - target: panelWindow - function onScreenChanged() { - root.screen = screen - - // If called from IPC always reposition if screen is updated - if (buttonName) { - setPosition() - } - Logger.d("NPanel", "OnScreenChanged", root.screen.name) - } - } - - color: Color.transparent - - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "noctalia-panel" - WlrLayershell.layer: useOverlay ? WlrLayer.Overlay : WlrLayer.Top - WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - Region { - id: maskRegion - } - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - } - } - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - - // Close any panel with Esc without requiring focus - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: root.close() - context: Qt.WindowShortcut - } - - // Clicking outside of the rectangle to close - MouseArea { + // The actual panel background and content + Item { anchors.fill: parent - enabled: root.backgroundClickEnabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.close() - } - // The actual panel's content - NShapedRectangle { - id: panelBackground + NShapedRectangle { + id: panelBackground - backgroundColor: (attachedToBar && (topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted)) ? Qt.alpha(panelBackgroundColor, Settings.data.bar.backgroundOpacity) : panelBackgroundColor + backgroundColor: root.attachedToBar ? Qt.alpha(root.panelBackgroundColor, Settings.data.bar.backgroundOpacity) : root.panelBackgroundColor - topLeftRadius: Style.radiusL - topRightRadius: Style.radiusL - bottomLeftRadius: Style.radiusL - bottomRightRadius: Style.radiusL + // Animation properties + opacity: root.animationProgress + scale: root.attachedToBar ? 1 : (0.95 + root.animationProgress * 0.05) + // Transform origin for scale animation + transformOrigin: { + // For detached panels, scale from center + if (!root.attachedToBar) { + return Item.Center + } - /*// Drop shadow effect - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowBlur: 0.85 - shadowOpacity: 0.45 - shadowColor: Color.mShadow - shadowHorizontalOffset: (barPosition === "left" || barPosition === "top") ? 6 : - 6 - shadowVerticalOffset: (barPosition === "left" || barPosition === "top") ? 6 : - 6 - }*/ - - // Set inverted corners based on panel anchors and bar position - - // Top-left corner - topLeftInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to top edge (bar is at top) - if (effectivePanelAnchorTop) - return true - // Or if panel is anchored to left edge (bar is at left) - if (effectivePanelAnchorLeft) - return true - return false - } - topLeftInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" - - // Top-right corner - topRightInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to top edge (bar is at top) - if (effectivePanelAnchorTop) - return true - // Or if panel is anchored to right edge (bar is at right) - if (effectivePanelAnchorRight) - return true - return false - } - topRightInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" - - // Bottom-left corner - bottomLeftInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to bottom edge (bar is at bottom) - if (effectivePanelAnchorBottom) - return true - // Or if panel is anchored to left edge (bar is at left) - if (effectivePanelAnchorLeft) - return true - return false - } - bottomLeftInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" - - // Bottom-right corner - bottomRightInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to bottom edge (bar is at bottom) - if (effectivePanelAnchorBottom) - return true - // Or if panel is anchored to right edge (bar is at right) - if (effectivePanelAnchorRight) - return true - return false - } - bottomRightInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" - - // Dragging support - property bool draggable: root.draggable - property bool isDragged: false - property real manualX: 0 - property real manualY: 0 - width: { - var w - if (root.preferredWidthRatio !== undefined) { - w = Math.round(Math.max(screen?.width * root.preferredWidthRatio, root.preferredWidth)) - } else { - w = root.preferredWidth - } - // Clamp width so it is never bigger than the screen - return Math.min(w, screen?.width - Style.marginL * 2) - } - height: { - var h - if (root.preferredHeightRatio !== undefined) { - h = Math.round(Math.max(screen?.height * root.preferredHeightRatio, root.preferredHeight)) - } else { - h = root.preferredHeight + // For bar-attached panels, scale from the edge touching the bar + if (root.barPosition === "top") + return Item.Top + if (root.barPosition === "bottom") + return Item.Bottom + if (root.barPosition === "left") + return Item.Left + if (root.barPosition === "right") + return Item.Right + return Item.Center } - // Clamp height so it is never bigger than the screen - return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) - } + topLeftRadius: Style.radiusL + topRightRadius: Style.radiusL + bottomLeftRadius: Style.radiusL + bottomRightRadius: Style.radiusL - opacity: root.panelBackgroundOpacity - x: isDragged ? manualX : calculatedX - y: isDragged ? manualY : calculatedY + // Inverted corners based on bar attachment + // When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted + topLeftInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + topRightInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + bottomLeftInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + bottomRightInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) - // Animate width and height changes smoothly - Behavior on width { - enabled: !panelBackground.isDragged - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - Behavior on height { - enabled: !panelBackground.isDragged - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - // --------------------------------------------- - // Does not account for corners are they are negligible and helps keep the code clean. - // --------------------------------------------- - property real marginTop: { - if (!barIsVisible) { - return 0 + // Set inverted corner direction based on which edge touches the bar + // For horizontal bars (top/bottom): left/right edges touch bar → horizontal curves + // For vertical bars (left/right): top/bottom edges touch bar → vertical curves + topLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + topRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + bottomLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + bottomRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + width: { + var w + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) { + w = contentLoader.item.contentPreferredWidth + } // Priority 2: Ratio-based size + else if (root.preferredWidthRatio !== undefined) { + w = Math.round(Math.max((parent.width || 1920) * root.preferredWidthRatio, root.preferredWidth)) + } // Priority 3: Static preferred width + else { + w = root.preferredWidth + } + return Math.min(w, (parent.width || 1920) - Style.marginL * 2) } - switch (barPosition) { - case "top": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginVertical * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginBottom: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "bottom": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginVertical * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginLeft: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "left": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginHorizontal * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginRight: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "right": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginHorizontal * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - // --------------------------------------------- - property int calculatedX: { - // Priority to fixed anchoring - if (panelAnchorHorizontalCenter) { - // Center horizontally but respect bar margins - var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) - var minX = marginLeft - var maxX = panelWindow.width - panelBackground.width - marginRight - return Math.round(Math.max(minX, Math.min(centerX, maxX))) - } else if (panelAnchorLeft) { - return marginLeft - } else if (panelAnchorRight) { - return Math.round(panelWindow.width - panelBackground.width - marginRight) + height: { + var h + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) { + h = contentLoader.item.contentPreferredHeight + } // Priority 2: Ratio-based size + else if (root.preferredHeightRatio !== undefined) { + h = Math.round(Math.max((parent.height || 1080) * root.preferredHeightRatio, root.preferredHeight)) + } // Priority 3: Static preferred height + else { + h = root.preferredHeight + } + return Math.min(h, (parent.height || 1080) - Style.barHeight - Style.marginL * 2) } - // No fixed anchoring - if (barIsVertical) { - // Vertical bar - if (barPosition === "right") { - // To the left of the right bar - return Math.round(panelWindow.width - panelBackground.width - marginRight) + // Animation offset for slide effect on bar-attached panels + readonly property real slideOffset: root.attachedToBar ? (1 - root.animationProgress) * 20 : 0 + + // Position the panel using explicit x/y coordinates (no anchors) + // This makes coordinates clearer for the click-through mask system + x: { + // If useButtonPosition is enabled, align panel X with button + // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid + // after the source panel (e.g., ControlCenter) closes, but we still have valid position data + if (root.useButtonPosition && parent.width > 0 && width > 0) { + if (root.barIsVertical) { + // For vertical bars + if (root.attachedToBar) { + // Attached panels: align with bar edge (left or right side) + if (root.barPosition === "left") { + // Panel to the right of left bar + var leftBarEdge = root.barMarginH + Style.barHeight + // Panel sits right at bar edge (inverted corners curve up/down) + // Slide from the bar when opening + // Shift left by 1px to eliminate any gap between bar and panel + return leftBarEdge - slideOffset - 1 + } else { + // right + // Panel to the left of right bar + var rightBarEdge = parent.width - root.barMarginH - Style.barHeight + // Panel sits right at bar edge (inverted corners curve up/down) + // Slide from the bar when opening + // Shift right by 1px to eliminate any gap between bar and panel + return rightBarEdge - width + slideOffset + 1 + } + } else { + // Detached panels: center on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 + // Clamp to screen bounds with margins + panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) + return panelX + } + } else { + // For horizontal bars, center panel on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 + // Clamp to bar bounds (account for floating bar margins) + // When attached, panel should not extend beyond bar edges + if (root.attachedToBar) { + // Inverted corners with horizontal direction extend left/right by radiusL + // When bar is floating, it also has rounded corners, so we need extra inset + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width)) + } else { + panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) + } + return panelX + } + } + + // Standard anchor positioning + Logger.d("NPanel", "Fallback to standard anchor positioning") + + if (root.panelAnchorHorizontalCenter) { + Logger.d("NPanel", " -> Horizontal center") + return (parent.width - width) / 2 + } else if (root.effectivePanelAnchorRight) { + Logger.d("NPanel", " -> Right anchor") + return parent.width - width - Style.marginL + } else if (root.effectivePanelAnchorLeft) { + Logger.d("NPanel", " -> Left anchor") + return Style.marginL } else { - // To the right of the left bar - return marginLeft - } - } else { - // Horizontal bar - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) - // Keep panel within screen bounds - var maxX = panelWindow.width - panelBackground.width - marginRight - var minX = marginLeft + // No explicit anchor: default to centering on bar + Logger.d("NPanel", " -> Default to center (no explicit anchor)") - if (Settings.data.bar.floating) { - maxX -= Settings.data.bar.marginHorizontal * Style.marginXL * 10 - minX += Settings.data.bar.marginHorizontal * Style.marginXL * 10 + // For horizontal bars: center horizontally + // For vertical bars: center horizontally in available space + if (root.barIsVertical) { + // Center in the space not occupied by the bar + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = parent.width - availableStart - Style.marginL + return availableStart + (availableWidth - width) / 2 + } else { + // right + var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL + return Style.marginL + (availableWidth - width) / 2 + } + } else { + // For horizontal bars: center horizontally, respect bar margins if attached + if (root.attachedToBar) { + // When attached, respect bar bounds (like button position does) + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + var centeredX = (parent.width - width) / 2 + return Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + } else { + return (parent.width - width) / 2 + } + } + } + } + + y: { + // If useButtonPosition is enabled, position panel relative to bar + // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid + // after the source panel (e.g., ControlCenter) closes, but we still have valid position data + if (root.useButtonPosition && parent.height > 0 && height > 0) { + if (root.barPosition === "top") { + // Panel below top bar + var topBarEdge = root.barMarginV + Style.barHeight + if (root.attachedToBar) { + // Panel sits right at bar edge (inverted corners curve to the sides) + // Slide from the bar when opening + // Shift up by 1px to eliminate any gap between bar and panel + return topBarEdge - slideOffset - 1 + } else { + return topBarEdge + Style.marginM + } + } else if (root.barPosition === "bottom") { + // Panel above bottom bar + var bottomBarEdge = parent.height - root.barMarginV - Style.barHeight + if (root.attachedToBar) { + // Panel sits right at bar edge (inverted corners curve to the sides) + // Slide from the bar when opening + // Shift down by 1px to eliminate any gap between bar and panel + return bottomBarEdge - height + slideOffset + 1 + } else { + return bottomBarEdge - height - Style.marginM + } + } else if (root.barIsVertical) { + // For vertical bars, center panel on button Y position + var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2 + // Clamp to bar bounds (account for floating bar margins and inverted corners) + var extraPadding = root.attachedToBar ? Style.radiusL : 0 + if (root.attachedToBar) { + // When attached, panel should not extend beyond bar edges (accounting for floating margins) + // Inverted corners with vertical direction extend up/down by radiusL + // When bar is floating, it also has rounded corners, so we need extra inset + var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0) + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - height)) + } else { + panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding)) + } + return panelY + } + } + + // Standard anchor positioning + // Calculate bar offset for detached panels - they should never overlap the bar + var barOffset = 0 + if (!root.attachedToBar) { + // For detached panels, always account for bar position + if (root.barPosition === "top") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM + } else if (root.barPosition === "bottom") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM } - return Math.round(Math.max(minX, Math.min(targetX, maxX))) } else { - // Fallback to center horizontally - return Math.round((panelWindow.width - panelBackground.width) / 2) - } - } - } - - // --------------------------------------------- - property int calculatedY: { - // Priority to fixed anchoring - if (panelAnchorVerticalCenter) { - // Center vertically but respect bar margins - var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) - var minY = marginTop - var maxY = panelWindow.height - panelBackground.height - marginBottom - return Math.round(Math.max(minY, Math.min(centerY, maxY))) - } else if (panelAnchorTop) { - return marginTop - } else if (panelAnchorBottom) { - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } - - // No fixed anchoring - if (barIsVertical) { - // Vertical bar - if (useButtonPosition) { - // Position panel relative to button - var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) - // Keep panel within screen bounds - var maxY = panelWindow.height - panelBackground.height - marginBottom - var minY = marginTop - - if (Settings.data.bar.floating) { - maxY -= Settings.data.bar.marginHorizontal * Style.marginXL * 10 - minY += Settings.data.bar.marginHorizontal * Style.marginXL * 10 + // For attached panels with explicit anchors + if (root.effectivePanelAnchorTop && root.barPosition === "top") { + // When attached to top bar: position right at bar edge (like useButtonPosition does) + // Shift up by 1px to eliminate gap between bar and panel + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { + // When attached to bottom bar: position right at bar edge + // Shift down by 1px to eliminate gap between bar and panel + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } else if (!root.hasExplicitVerticalAnchor) { + // No explicit vertical anchor AND attached: default to attaching to bar edge + if (root.barPosition === "top") { + // Attach to top bar + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + // Attach to bottom bar + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + // For vertical bars with no explicit anchor: center vertically on bar + // This is handled in the else block below } + } - return Math.round(Math.max(minY, Math.min(targetY, maxY))) + if (root.panelAnchorVerticalCenter) { + return (parent.height - height) / 2 + } else if (root.effectivePanelAnchorTop) { + return barOffset + Style.marginL + } else if (root.effectivePanelAnchorBottom) { + return parent.height - height - barOffset - Style.marginL } else { - // Fallback to center vertically - return Math.round((panelWindow.height - panelBackground.height) / 2) - } - } else { - // Horizontal bar - if (barPosition === "bottom") { - // Above the bottom bar - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } else { - // Below the top bar - return marginTop - } - } - } - - // Animate in when component is completed - Component.onCompleted: { - // Start invisible - // Use a timer to delay the animation start, allowing QML to properly set up initial state - fadeInTimer.start() - } - - Timer { - id: fadeInTimer - interval: 1 - repeat: false - onTriggered: { - // Fade in background - root.panelBackgroundOpacity = 1.0 - } - } - - // Timer to fade in content after slide animation completes - Timer { - id: contentFadeInTimer - interval: Style.animationFast - repeat: false - running: true - onTriggered: root.panelContentOpacity = 1.0 - } - - // Reset drag position when panel closes - Connections { - target: root - function onClosed() { - panelBackground.isDragged = false - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behavior - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - - Loader { - id: panelContentLoader - anchors.fill: parent - sourceComponent: root.panelContent - opacity: root.panelContentOpacity - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - - // Allow content to dynamically resize the panel - onItemChanged: { - if (item) { - // Bind to content's preferredWidth/Height if they exist - if (item.hasOwnProperty('contentPreferredWidth')) { - root.preferredWidth = Qt.binding(() => item.contentPreferredWidth) - } - if (item.hasOwnProperty('contentPreferredHeight')) { - root.preferredHeight = Qt.binding(() => item.contentPreferredHeight) - } - if (item.hasOwnProperty('contentPreferredWidthRatio')) { - root.preferredWidthRatio = Qt.binding(() => item.contentPreferredWidthRatio) - } - if (item.hasOwnProperty('contentPreferredHeightRatio')) { - root.preferredHeightRatio = Qt.binding(() => item.contentPreferredHeightRatio) + // No explicit vertical anchor + if (root.barIsVertical) { + // For vertical bars: center vertically on bar + if (root.attachedToBar) { + // When attached, respect bar bounds + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + var centeredY = (parent.height - height) / 2 + return Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) + } else { + return (parent.height - height) / 2 + } + } else { + // For horizontal bars: attach to bar edge by default + if (root.attachedToBar) { + if (root.barPosition === "top") { + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + } + // Detached or no bar position: use default positioning + if (root.barPosition === "top") { + return barOffset + Style.marginL + } else if (root.barPosition === "bottom") { + return Style.marginL + } else { + return Style.marginL + } } } } - } - // Handle drag move on the whole panel area - DragHandler { - id: dragHandler - target: null - enabled: panelBackground.draggable - property real dragStartX: 0 - property real dragStartY: 0 - onActiveChanged: { - if (active) { - // Capture current position into manual coordinates BEFORE toggling isDragged - panelBackground.manualX = panelBackground.x - panelBackground.manualY = panelBackground.y - dragStartX = panelBackground.x - dragStartY = panelBackground.y - panelBackground.isDragged = true - if (root.enableBackgroundClick) - root.disableBackgroundClick() - } else { - // Keep isDragged true so we continue using the manual x/y after release - if (root.enableBackgroundClick) - root.enableBackgroundClick() - } - } - onTranslationChanged: { - // Proposed new coordinates from fixed drag origin - var nx = dragStartX + translation.x - var ny = dragStartY + translation.y - - // Calculate gaps so we never overlap the bar on any side - var baseGap = Style.marginS - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 - - var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) - var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) - var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) - var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) - - // Clamp within screen bounds accounting for insets - var maxX = panelWindow.width - panelBackground.width - insetRight - var minX = insetLeft - var maxY = panelWindow.height - panelBackground.height - insetBottom - var minY = insetTop - - panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) - panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) - } - } - - // Drag indicator border - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Style.borderM - radius: Style.radiusL - visible: panelBackground.isDragged && dragHandler.active - opacity: 0.8 - z: 3000 - - // Subtle glow effect - Rectangle { + // MouseArea to catch clicks on the panel and prevent them from reaching the background + // This prevents closing the panel when clicking inside it + MouseArea { anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Style.borderS - radius: Style.radiusL - opacity: 0.3 + z: -1 // Behind content, but on the panel background + onClicked: { + + // Accept and ignore - prevents propagation to background + } + } + + // Panel content loader + Loader { + id: contentLoader + anchors.fill: parent + sourceComponent: root.panelContent } } } diff --git a/Widgets/NShapedRectangle.qml b/Widgets/NShapedRectangle.qml index 36f76192..9da7ebb4 100644 --- a/Widgets/NShapedRectangle.qml +++ b/Widgets/NShapedRectangle.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Effects import qs.Commons Item { @@ -24,8 +25,17 @@ Item { property string bottomLeftInvertedDirection: "horizontal" // default: curves left property string bottomRightInvertedDirection: "horizontal" // default: curves right - // Background color - property color backgroundColor: "white" + // Background color and borders + property color backgroundColor: Color.mPrimary + property color borderColor: Color.mOutline + property int borderWidth: Style.borderS + + // Shadow properties + property bool shadowEnabled: true + property real shadowOpacity: 1.0 // 0.0 to 1.0 + property real shadowBlur: 0.9 + property real shadowHorizontalOffset: 3 + property real shadowVerticalOffset: 3 // Check if any corner is inverted readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted @@ -36,144 +46,174 @@ Item { readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0) readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0) - // Simple rectangle for non-inverted corners (better performance) - Rectangle { - id: simpleBackground + // Background layer: shape with shadow effects (layer.enabled) + Item { + id: shadowLayer anchors.fill: parent - color: root.backgroundColor - radius: topLeftRadius // Use topLeftRadius as default - border.width: Style.borderS - border.color: Color.mOutline - visible: !root.hasInvertedCorners + z: 0 - topLeftRadius: root.topLeftRadius - topRightRadius: root.topRightRadius - bottomLeftRadius: root.bottomLeftRadius - bottomRightRadius: root.bottomRightRadius - } + // Apply shadow effect to this layer only + layer.enabled: root.shadowEnabled + layer.smooth: true + // layer.textureSize: Qt.size(width * Screen.devicePixelRatio, height * Screen.devicePixelRatio) + layer.effect: MultiEffect { + shadowEnabled: root.shadowEnabled + shadowOpacity: root.shadowOpacity + shadowColor: "#000000" + shadowHorizontalOffset: root.shadowHorizontalOffset + shadowVerticalOffset: root.shadowVerticalOffset + blur: root.shadowBlur + blurMax: 32 + } - // Background with custom corners (for inverted corners) - Canvas { - id: background - anchors.fill: parent - anchors.topMargin: -root.topPadding - anchors.bottomMargin: -root.bottomPadding - anchors.leftMargin: -root.leftPadding - anchors.rightMargin: -root.rightPadding - visible: root.hasInvertedCorners + // Simple rectangle for non-inverted corners (better performance) + Rectangle { + id: simpleBackground + anchors.fill: parent + color: root.backgroundColor + radius: topLeftRadius // Use topLeftRadius as default + border.width: borderWidth + border.color: borderColor + visible: !root.hasInvertedCorners - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true + topLeftRadius: root.topLeftRadius + topRightRadius: root.topRightRadius + bottomLeftRadius: root.bottomLeftRadius + bottomRightRadius: root.bottomRightRadius + } - onPaint: { - var ctx = getContext("2d") - ctx.reset() + // Background with custom corners (for inverted corners) + Canvas { + id: background + anchors.fill: parent + anchors.topMargin: -root.topPadding + anchors.bottomMargin: -root.bottomPadding + anchors.leftMargin: -root.leftPadding + anchors.rightMargin: -root.rightPadding + visible: root.hasInvertedCorners - // Adjust coordinates to account for inverted corner padding - var x = root.leftPadding - var y = root.topPadding - var w = width - root.leftPadding - root.rightPadding - var h = height - root.topPadding - root.bottomPadding + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true - ctx.fillStyle = root.backgroundColor - ctx.beginPath() + onPaint: { + var ctx = getContext("2d") + ctx.reset() - // Start from top left - if (topLeftInverted) { - if (topLeftInvertedDirection === "vertical") { - ctx.moveTo(x, y) + // Adjust coordinates to account for inverted corner padding + var x = root.leftPadding + var y = root.topPadding + var w = width - root.leftPadding - root.rightPadding + var h = height - root.topPadding - root.bottomPadding + + ctx.fillStyle = root.backgroundColor + ctx.beginPath() + + // Start from top left + if (topLeftInverted) { + if (topLeftInvertedDirection === "vertical") { + ctx.moveTo(x, y) + } else { + ctx.moveTo(x + topLeftRadius, y) + } } else { ctx.moveTo(x + topLeftRadius, y) } - } else { - ctx.moveTo(x + topLeftRadius, y) - } - // Top edge and top right corner - if (topRightInverted) { - if (topRightInvertedDirection === "horizontal") { - // Curves to the right - ctx.lineTo(x + w, y) - ctx.lineTo(x + w + topRightRadius, y) - ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) + // Top edge and top right corner + if (topRightInverted) { + if (topRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y) + ctx.lineTo(x + w + topRightRadius, y) + ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) + } else { + // Curves upward + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y - topRightRadius) + ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + topRightRadius) + } } else { - // Curves upward - ctx.lineTo(x + w, y) - ctx.lineTo(x + w, y - topRightRadius) - ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y) - ctx.lineTo(x + w, y) - ctx.lineTo(x + w, y + topRightRadius) + ctx.lineTo(x + w - topRightRadius, y) + ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius) } - } else { - ctx.lineTo(x + w - topRightRadius, y) - ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius) - } - // Right edge and bottom right corner - if (bottomRightInverted) { - if (bottomRightInvertedDirection === "horizontal") { - // Curves to the right + // Right edge and bottom right corner + if (bottomRightInverted) { + if (bottomRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y + h - bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - bottomRightRadius, y + h) + } else { + // Curves downward + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w, y + h + bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h) + } + } else { ctx.lineTo(x + w, y + h - bottomRightRadius) - ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h) - ctx.lineTo(x + w, y + h) - ctx.lineTo(x + w - bottomRightRadius, y + h) - } else { - // Curves downward - ctx.lineTo(x + w, y + h) - ctx.lineTo(x + w, y + h + bottomRightRadius) - ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h) + ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius) } - } else { - ctx.lineTo(x + w, y + h - bottomRightRadius) - ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius) - } - // Bottom edge and bottom left corner - if (bottomLeftInverted) { - if (bottomLeftInvertedDirection === "horizontal") { - // Curves to the left + // Bottom edge and bottom left corner + if (bottomLeftInverted) { + if (bottomLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x + bottomLeftRadius, y + h) + ctx.lineTo(x - bottomLeftRadius, y + h) + ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) + } else { + // Curves downward + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h + bottomLeftRadius) + ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h - bottomLeftRadius) + } + } else { ctx.lineTo(x + bottomLeftRadius, y + h) - ctx.lineTo(x - bottomLeftRadius, y + h) - ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) - } else { - // Curves downward - ctx.lineTo(x, y + h) - ctx.lineTo(x, y + h + bottomLeftRadius) - ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) - ctx.lineTo(x, y + h) - ctx.lineTo(x, y + h - bottomLeftRadius) + ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius) } - } else { - ctx.lineTo(x + bottomLeftRadius, y + h) - ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius) - } - // Left edge and back to top left corner - if (topLeftInverted) { - if (topLeftInvertedDirection === "horizontal") { - // Curves to the left - ctx.lineTo(x, y + topLeftRadius) - ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) - ctx.lineTo(x, y) - ctx.lineTo(x + topLeftRadius, y) + // Left edge and back to top left corner + if (topLeftInverted) { + if (topLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x, y + topLeftRadius) + ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) + ctx.lineTo(x, y) + ctx.lineTo(x + topLeftRadius, y) + } else { + // Curves upward + ctx.lineTo(x, y + topLeftRadius) + ctx.lineTo(x, y) + ctx.lineTo(x, y - topLeftRadius) + ctx.quadraticCurveTo(x, y, x + topLeftRadius, y) + } } else { - // Curves upward ctx.lineTo(x, y + topLeftRadius) - ctx.lineTo(x, y) - ctx.lineTo(x, y - topLeftRadius) - ctx.quadraticCurveTo(x, y, x + topLeftRadius, y) + ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius) } - } else { - ctx.lineTo(x, y + topLeftRadius) - ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius) - } - ctx.closePath() - ctx.fill() + ctx.closePath() + ctx.fill() + } } } + // Content layer: for child elements (NO layer effects - keeps text sharp) + // Child components can be added here and will render on top without blur + default property alias contentChildren: contentLayer.data + Item { + id: contentLayer + anchors.fill: parent + z: 1 + } + // Trigger repaint when properties change onTopLeftRadiusChanged: background.requestPaint() onTopRightRadiusChanged: background.requestPaint() diff --git a/shell.qml b/shell.qml index 04ade2a6..e8513daf 100644 --- a/shell.qml +++ b/shell.qml @@ -75,6 +75,73 @@ ShellRoot { } } + // ------------------------------ + // Define panel components (must be at ShellRoot level for NFullScreenWindow access) + Component { + id: launcherComponent + Launcher {} + } + + Component { + id: controlCenterComponent + ControlCenterPanel {} + } + + Component { + id: calendarComponent + CalendarPanel {} + } + + Component { + id: settingsComponent + SettingsPanel {} + } + + Component { + id: directWidgetSettingsComponent + DirectWidgetSettingsPanel {} + } + + Component { + id: notificationHistoryComponent + NotificationHistoryPanel {} + } + + Component { + id: sessionMenuComponent + SessionMenu {} + } + + Component { + id: wifiComponent + WiFiPanel {} + } + + Component { + id: bluetoothComponent + BluetoothPanel {} + } + + Component { + id: audioComponent + AudioPanel {} + } + + Component { + id: wallpaperComponent + WallpaperPanel {} + } + + Component { + id: batteryComponent + BatteryPanel {} + } + + Component { + id: barComp + Bar {} + } + Loader { active: i18nLoaded && settingsLoaded @@ -99,8 +166,7 @@ ShellRoot { Background {} Overview {} - ScreenCorners {} - Bar {} + Dock {} Notification { @@ -121,67 +187,118 @@ ShellRoot { // IPCService is treated as a service // but it's actually an Item that needs to exists in the shell. IPCService {} + } + } - // ------------------------------ - // All the NPanels - Launcher { - id: launcherPanel - objectName: "launcherPanel" + // ------------------------------ + // NFullScreenWindow for each screen (manages bar + all panels) + // Wrapped in Loader to optimize memory - only loads when screen needs it + Variants { + model: Quickshell.screens + delegate: Item { + required property ShellScreen modelData + + property bool shouldBeActive: { + if (!i18nLoaded || !settingsLoaded) + return false + if (!BarService.isVisible) + return false + if (!modelData || !modelData.name) + return false + + var monitors = Settings.data.bar.monitors || [] + var result = monitors.length === 0 || monitors.includes(modelData.name) + + Logger.d("Shell", "NFullScreenWindow Loader for", modelData?.name, "- shouldBeActive:", result, "- monitors:", JSON.stringify(monitors)) + return result } - ControlCenterPanel { - id: controlCenterPanel - objectName: "controlCenterPanel" + property bool windowLoaded: false + + Loader { + id: windowLoader + active: parent.shouldBeActive + asynchronous: false + + property ShellScreen loaderScreen: modelData + + onLoaded: { + // Signal that window is loaded so exclusion zone can be created + parent.windowLoaded = true + } + + sourceComponent: NFullScreenWindow { + screen: windowLoader.loaderScreen + + // Register all panel components + panelComponents: [{ + "id": "launcherPanel", + "component": launcherComponent, + "zIndex": 50 + }, { + "id": "controlCenterPanel", + "component": controlCenterComponent, + "zIndex": 50 + }, { + "id": "calendarPanel", + "component": calendarComponent, + "zIndex": 50 + }, { + "id": "settingsPanel", + "component": settingsComponent, + "zIndex": 50 + }, { + "id": "directWidgetSettingsPanel", + "component": directWidgetSettingsComponent, + "zIndex": 50 + }, { + "id": "notificationHistoryPanel", + "component": notificationHistoryComponent, + "zIndex": 50 + }, { + "id": "sessionMenuPanel", + "component": sessionMenuComponent, + "zIndex": 50 + }, { + "id": "wifiPanel", + "component": wifiComponent, + "zIndex": 50 + }, { + "id": "bluetoothPanel", + "component": bluetoothComponent, + "zIndex": 50 + }, { + "id": "audioPanel", + "component": audioComponent, + "zIndex": 50 + }, { + "id": "wallpaperPanel", + "component": wallpaperComponent, + "zIndex": 50 + }, { + "id": "batteryPanel", + "component": batteryComponent, + "zIndex": 50 + }] + + // Bar component + barComponent: barComp + } } - CalendarPanel { - id: calendarPanel - objectName: "calendarPanel" - } + // BarExclusionZone - created after NFullScreenWindow has fully loaded + // Must also be disabled when bar is disabled (follows shouldBeActive) + Loader { + active: parent.windowLoaded && parent.shouldBeActive + asynchronous: false - SettingsPanel { - id: settingsPanel - objectName: "settingsPanel" - } + sourceComponent: BarExclusionZone { + screen: modelData + } - DirectWidgetSettingsPanel { - id: directWidgetSettingsPanel - objectName: "directWidgetSettingsPanel" - } - - NotificationHistoryPanel { - id: notificationHistoryPanel - objectName: "notificationHistoryPanel" - } - - SessionMenu { - id: sessionMenuPanel - objectName: "sessionMenuPanel" - } - - WiFiPanel { - id: wifiPanel - objectName: "wifiPanel" - } - - BluetoothPanel { - id: bluetoothPanel - objectName: "bluetoothPanel" - } - - AudioPanel { - id: audioPanel - objectName: "audioPanel" - } - - WallpaperPanel { - id: wallpaperPanel - objectName: "wallpaperPanel" - } - - BatteryPanel { - id: batteryPanel - objectName: "batteryPanel" + onLoaded: { + Logger.d("Shell", "BarExclusionZone created for", modelData?.name) + } } } }