From 49e0acb377d91465437a1470b1a851712cb464b8 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 00:12:25 +0200 Subject: [PATCH 01/35] Fix notification hide issue --- Modules/Notification/Notification.qml | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index e18c969..a79d60d 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -47,13 +47,29 @@ Variants { // Connect to animation signal from service Component.onCompleted: { NotificationService.animateAndRemove.connect(function (notification, index) { - // Find the delegate and trigger its animation - if (notificationStack.children && notificationStack.children[index]) { - let delegate = notificationStack.children[index] - if (delegate && delegate.animateOut) { - delegate.animateOut() + // Prefer lookup by identity to avoid index mismatches + var delegate = null + if (notificationStack.children && notificationStack.children.length > 0) { + for (var i = 0; i < notificationStack.children.length; i++) { + var child = notificationStack.children[i] + if (child && child.model && child.model.rawNotification === notification) { + delegate = child + break + } } } + + // Fallback to index if identity lookup failed + if (!delegate && notificationStack.children && notificationStack.children[index]) { + delegate = notificationStack.children[index] + } + + if (delegate && delegate.animateOut) { + delegate.animateOut() + } else { + // As a last resort, force-remove without animation to avoid stuck popups + NotificationService.forceRemoveNotification(notification) + } }) } From 170502259e5016d4b32b795df2dbfdaa8cd58446 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 19 Aug 2025 18:41:11 -0400 Subject: [PATCH 02/35] Removed border on bar media display to match the other capsules --- Modules/Bar/MediaMini.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index d41451f..5a276c8 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -31,8 +31,6 @@ Row { height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Math.round(Style.borderS * scaling)) anchors.verticalCenter: parent.verticalCenter From faafdc064414c94e045fe4991bba0972d0091022 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Tue, 19 Aug 2025 19:32:05 -0400 Subject: [PATCH 03/35] No need to check monitors all the times --- Commons/Settings.qml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index bc5d6c4..6fb29dd 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -82,16 +82,19 @@ Singleton { reload() } onLoaded: function () { - Logger.log("Settings", "OnLoaded") Qt.callLater(function () { - // Only set wallpaper on initial load, not on reloads - if (isInitialLoad && adapter.wallpaper.current !== "") { - Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) - WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) - } + if (isInitialLoad) { + Logger.log("Settings", "OnLoaded") + // Only set wallpaper on initial load, not on reloads + if (adapter.wallpaper.current !== "") { + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) + } - // Validate monitor configurations - if none of the configured monitors exist, clear the lists - validateMonitorConfigurations() + // Validate monitor configurations, only once + // if none of the configured monitors exist, clear the lists + validateMonitorConfigurations() + } isInitialLoad = false }) From 7ce8e51fa3e367b44195fc6aa93d485055776d58 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 12:42:35 +0200 Subject: [PATCH 04/35] Fix brightness ipc call --- Modules/IPC/IPCManager.qml | 1 + Services/BrightnessService.qml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 4c9a02c..638bc8e 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell.Io +import qs.Services Item { id: root diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 6770a2f..2295451 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -27,6 +27,15 @@ Singleton { return methods } + // Global helpers for IPC and shortcuts + function increaseBrightness(): void { + monitors.forEach(m => m.increaseBrightness()) + } + + function decreaseBrightness(): void { + monitors.forEach(m => m.decreaseBrightness()) + } + function getDetectedDisplays(): list { return detectedDisplays } From c8a93d7d1e1c193db28ecee74ad3523b8c0fdecb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 06:57:47 -0400 Subject: [PATCH 05/35] system-stats.sh compatible with nix and zenpower --- Bin/system-stats.sh | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Bin/system-stats.sh b/Bin/system-stats.sh index 7f628ed..f30d497 100755 --- a/Bin/system-stats.sh +++ b/Bin/system-stats.sh @@ -1,7 +1,6 @@ -#!/bin/bash +#!/usr/bin/env -S bash # A Bash script to monitor system stats and output them in JSON format. -# This script is a conversion of ZigStat # --- Configuration --- # Default sleep duration in seconds. Can be overridden by the first argument. @@ -114,7 +113,7 @@ get_cpu_temp() { local name name=$(<"$dir/name") # Check for supported sensor types. - if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then + if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then TEMP_SENSOR_PATH=$dir TEMP_SENSOR_TYPE=$name break # Found it, no need to keep searching. @@ -172,6 +171,24 @@ get_cpu_temp() { else echo 0 # Fallback fi + elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then + # For zenpower, read the first available temp sensor + for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do + if [[ -f "$temp_file" ]]; then + local temp_value + temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines + echo "$((temp_value / 1000))" + return + fi + done + echo 0 + + if [[ -f "$tctl_input" ]]; then + # Read the temperature and convert from millidegrees to degrees. + echo "$(($(<"$tctl_input") / 1000))" + else + echo 0 # Fallback + fi else echo 0 # Should not happen if cache logic is correct. fi From 044da177634a49e32af17dc5f30cf77a5fbee923 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 13:19:39 +0200 Subject: [PATCH 06/35] Refactor AppLauncher, fix clipboardhistory, add persistent history (max 50 entries), add advanced math stuff --- Helpers/AdvancedMath.js | 152 ++++++++++ Modules/AppLauncher/AppLauncher.qml | 351 +++++++---------------- Modules/AppLauncher/Calculator.qml | 161 +++++++++++ Modules/AppLauncher/ClipboardHistory.qml | 158 ++++++++++ Services/ClipboardService.qml | 131 +++++++-- 5 files changed, 690 insertions(+), 263 deletions(-) create mode 100644 Helpers/AdvancedMath.js create mode 100644 Modules/AppLauncher/Calculator.qml create mode 100644 Modules/AppLauncher/ClipboardHistory.qml diff --git a/Helpers/AdvancedMath.js b/Helpers/AdvancedMath.js new file mode 100644 index 0000000..9e9384e --- /dev/null +++ b/Helpers/AdvancedMath.js @@ -0,0 +1,152 @@ +// AdvancedMath.js - Lightweight math library for Noctalia Calculator +// Provides advanced mathematical functions beyond basic arithmetic + +// Helper function to convert degrees to radians +function toRadians(degrees) { + return degrees * (Math.PI / 180); +} + +// Helper function to convert radians to degrees +function toDegrees(radians) { + return radians * (180 / Math.PI); +} + +// Constants +var constants = { + PI: Math.PI, + E: Math.E, + LN2: Math.LN2, + LN10: Math.LN10, + LOG2E: Math.LOG2E, + LOG10E: Math.LOG10E, + SQRT1_2: Math.SQRT1_2, + SQRT2: Math.SQRT2 +}; + +// Safe evaluation function that handles advanced math +function evaluate(expression) { + try { + // Replace mathematical constants + var processed = expression + .replace(/\bpi\b/gi, Math.PI) + .replace(/\be\b/gi, Math.E); + + // Replace function calls with Math object equivalents + processed = processed + // Trigonometric functions + .replace(/\bsin\s*\(/g, 'Math.sin(') + .replace(/\bcos\s*\(/g, 'Math.cos(') + .replace(/\btan\s*\(/g, 'Math.tan(') + .replace(/\basin\s*\(/g, 'Math.asin(') + .replace(/\bacos\s*\(/g, 'Math.acos(') + .replace(/\batan\s*\(/g, 'Math.atan(') + .replace(/\batan2\s*\(/g, 'Math.atan2(') + + // Hyperbolic functions + .replace(/\bsinh\s*\(/g, 'Math.sinh(') + .replace(/\bcosh\s*\(/g, 'Math.cosh(') + .replace(/\btanh\s*\(/g, 'Math.tanh(') + .replace(/\basinh\s*\(/g, 'Math.asinh(') + .replace(/\bacosh\s*\(/g, 'Math.acosh(') + .replace(/\batanh\s*\(/g, 'Math.atanh(') + + // Logarithmic and exponential functions + .replace(/\blog\s*\(/g, 'Math.log10(') + .replace(/\bln\s*\(/g, 'Math.log(') + .replace(/\bexp\s*\(/g, 'Math.exp(') + .replace(/\bpow\s*\(/g, 'Math.pow(') + + // Root functions + .replace(/\bsqrt\s*\(/g, 'Math.sqrt(') + .replace(/\bcbrt\s*\(/g, 'Math.cbrt(') + + // Rounding and absolute + .replace(/\babs\s*\(/g, 'Math.abs(') + .replace(/\bfloor\s*\(/g, 'Math.floor(') + .replace(/\bceil\s*\(/g, 'Math.ceil(') + .replace(/\bround\s*\(/g, 'Math.round(') + .replace(/\btrunc\s*\(/g, 'Math.trunc(') + + // Min/Max + .replace(/\bmin\s*\(/g, 'Math.min(') + .replace(/\bmax\s*\(/g, 'Math.max(') + + // Random + .replace(/\brandom\s*\(\s*\)/g, 'Math.random()'); + + // Handle degree versions of trig functions + processed = processed + .replace(/\bsind\s*\(/g, '(function(x) { return Math.sin(' + (Math.PI / 180) + ' * x); })(') + .replace(/\bcosd\s*\(/g, '(function(x) { return Math.cos(' + (Math.PI / 180) + ' * x); })(') + .replace(/\btand\s*\(/g, '(function(x) { return Math.tan(' + (Math.PI / 180) + ' * x); })('); + + // Sanitize expression (only allow safe characters) + if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) { + throw new Error("Invalid characters in expression"); + } + + // Evaluate the processed expression + var result = eval(processed); + + if (!isFinite(result) || isNaN(result)) { + throw new Error("Invalid result"); + } + + return result; + } catch (error) { + throw new Error("Evaluation failed: " + error.message); + } +} + +// Format result for display +function formatResult(result) { + if (Number.isInteger(result)) { + return result.toString(); + } + + // Handle very large or very small numbers + if (Math.abs(result) >= 1e15 || (Math.abs(result) < 1e-6 && result !== 0)) { + return result.toExponential(6); + } + + // Normal decimal formatting + return parseFloat(result.toFixed(10)).toString(); +} + +// Get list of available functions for help +function getAvailableFunctions() { + return [ + // Basic arithmetic: +, -, *, /, %, ^, () + + // Trigonometric functions + "sin(x), cos(x), tan(x) - trigonometric functions (radians)", + "sind(x), cosd(x), tand(x) - trigonometric functions (degrees)", + "asin(x), acos(x), atan(x) - inverse trigonometric", + "atan2(y, x) - two-argument arctangent", + + // Hyperbolic functions + "sinh(x), cosh(x), tanh(x) - hyperbolic functions", + "asinh(x), acosh(x), atanh(x) - inverse hyperbolic", + + // Logarithmic and exponential + "log(x) - base 10 logarithm", + "ln(x) - natural logarithm", + "exp(x) - e^x", + "pow(x, y) - x^y", + + // Root functions + "sqrt(x) - square root", + "cbrt(x) - cube root", + + // Rounding and absolute + "abs(x) - absolute value", + "floor(x), ceil(x), round(x), trunc(x)", + + // Min/Max/Random + "min(a, b, ...), max(a, b, ...)", + "random() - random number 0-1", + + // Constants + "pi, e - mathematical constants" + ]; +} diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index e7bae91..58f0c2c 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -15,30 +15,125 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort NLoader { id: appLauncher isLoaded: false - // Clipboard state is persisted in Services/ClipboardService.qml + content: Component { NPanel { id: appLauncherPanel WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // No local timer/processes; use persistent Clipboard service - - // Removed local clipboard processes; handled by Clipboard service - - // Copy helpers via simple exec; avoid keeping processes alive locally - function copyImageBase64(mime, base64) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + // Import modular components + Calculator { + id: calculator } - function copyText(text) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + ClipboardHistory { + id: clipboardHistory } - function updateClipboardHistory() { - ClipboardService.refresh() + // Properties + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + + // Refresh clipboard when user starts typing clipboard commands + onSearchTextChanged: { + if (searchText.startsWith(">clip")) { + clipboardHistory.refresh() + } } + // Main filtering logic + property var filteredEntries: { + Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + Logger.log("AppLauncher", "No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : "" + var results = [] + + // Handle special commands + if (query === ">") { + results.push({ + "isCommand": true, + "name": ">calc", + "content": "Calculator - evaluate mathematical expressions", + "icon": "calculate", + "execute": executeCalcCommand + }) + + results.push({ + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": executeClipCommand + }) + + return results + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + return clipboardHistory.processQuery(query) + } + + // Handle calculator + if (query.startsWith(">calc")) { + return calculator.processQuery(query, "calc") + } + + // Handle direct math expressions after ">" + if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) { + const mathResults = calculator.processQuery(query, "direct") + if (mathResults.length > 0) { + return mathResults + } + // If math evaluation fails, fall through to regular search + } + + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + })) + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + "keys": ["name", "comment", "genericName"] + }) + results = results.concat(fuzzyResults.map(function (r) { + return r.obj + })) + } + + Logger.log("AppLauncher", "Filtered entries:", results.length) + return results + } + + // Command execution functions + function executeCalcCommand() { + searchText = ">calc " + searchInput.cursorPosition = searchText.length + } + + function executeClipCommand() { + searchText = ">clip " + searchInput.cursorPosition = searchText.length + } + + // Navigation functions function selectNext() { if (filteredEntries.length > 0) { selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) @@ -67,233 +162,6 @@ NLoader { } } - property var desktopEntries: DesktopEntries.applications.values - property string searchText: "" - property int selectedIndex: 0 - - // Refresh clipboard when user starts typing clipboard commands - onSearchTextChanged: { - if (searchText.startsWith(">clip")) { - ClipboardService.refresh() - } - } - property var filteredEntries: { - Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) - if (!desktopEntries || desktopEntries.length === 0) { - Logger.log("AppLauncher", "No desktop entries available") - return [] - } - - // Filter out entries that shouldn't be displayed - var visibleEntries = desktopEntries.filter(entry => { - if (!entry || entry.noDisplay) { - return false - } - return true - }) - - Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) - - var query = searchText ? searchText.toLowerCase() : "" - var results = [] - - // Handle special commands - if (query === ">") { - results.push({ - "isCommand": true, - "name": ">calc", - "content": "Calculator - evaluate mathematical expressions", - "icon": "tag", - "execute": function () { - searchText = ">calc " - searchInput.cursorPosition = searchText.length - } - }) - - results.push({ - "isCommand": true, - "name": ">clip", - "content": "Clipboard history - browse and restore clipboard items", - "icon": "content_paste", - "execute": function () { - searchText = ">clip " - searchInput.cursorPosition = searchText.length - } - }) - - return results - } - - // Handle clipboard history - if (query.startsWith(">clip")) { - const searchTerm = query.slice(5).trim() - - ClipboardService.history.forEach(function (clip, index) { - let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip - - if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { - let entry - if (clip.type === 'image') { - entry = { - "isClipboard": true, - "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), - "content": "Image: " + clip.mimeType, - "icon": "image", - "type": 'image', - "data": clip.data, - "execute": function () { - const base64Data = clip.data.split(',')[1] - copyImageBase64(clip.mimeType, base64Data) - Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) - } - } - } else { - const textContent = clip.content || clip - let displayContent = textContent - let previewContent = "" - - displayContent = displayContent.replace(/\s+/g, ' ').trim() - - if (displayContent.length > 50) { - previewContent = displayContent - displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." - } - - entry = { - "isClipboard": true, - "name": displayContent, - "content": previewContent || textContent, - "icon": "content_paste", - "execute": function () { - Quickshell.clipboardText = String(textContent) - copyText(String(textContent)) - var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent - Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) - } - } - } - results.push(entry) - } - }) - - if (results.length === 0) { - results.push({ - "isClipboard": true, - "name": "No clipboard history", - "content": "No matching clipboard entries found", - "icon": "content_paste_off" - }) - } - - return results - } - - // Handle direct math expressions after ">" - if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) { - var mathExpr = query.slice(1).trim() - // Check if it looks like a math expression (contains numbers and math operators) - if (mathExpr && /[0-9+\-*/().]/.test(mathExpr)) { - try { - var sanitizedExpr = mathExpr.replace(/[^0-9+\-*/().\s]/g, '') - var result = eval(sanitizedExpr) - - if (isFinite(result) && !isNaN(result)) { - var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '') - results.push({ - "isCalculator": true, - "name": `${mathExpr} = ${displayResult}`, - "result": result, - "expr": mathExpr, - "icon": "tag", - "execute": function () { - Quickshell.clipboardText = displayResult - copyText(displayResult) - Quickshell.execDetached( - ["notify-send", "Calculator", `${mathExpr} = ${displayResult} (copied to clipboard)`]) - } - }) - return results - } - } catch (error) { - // If math evaluation fails, fall through to regular search - } - } - } - - // Handle calculator - if (query.startsWith(">calc")) { - var expr = searchText.slice(5).trim() - if (expr && expr !== "") { - try { - // Simple evaluation - only allow basic math operations - var sanitizedExpr = expr.replace(/[^0-9+\-*/().\s]/g, '') - var result = eval(sanitizedExpr) - - if (isFinite(result) && !isNaN(result)) { - var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, - '') - results.push({ - "isCalculator": true, - "name": `${expr} = ${displayResult}`, - "result": result, - "expr": expr, - "icon": "tag", - "execute": function () { - Quickshell.clipboardText = displayResult - copyText(displayResult) - Quickshell.execDetached( - ["notify-send", "Calculator", `${expr} = ${displayResult} (copied to clipboard)`]) - } - }) - } else { - results.push({ - "isCalculator": true, - "name": "Invalid expression", - "content": "Please enter a valid mathematical expression", - "icon": "tag", - "execute": function () {} - }) - } - } catch (error) { - results.push({ - "isCalculator": true, - "name": "Invalid expression", - "content": "Please enter a valid mathematical expression", - "icon": "tag", - "execute": function () {} - }) - } - } else { - // Show placeholder when just ">calc" is entered - results.push({ - "isCalculator": true, - "name": "Calculator", - "content": "Enter a mathematical expression (e.g., 5+5, 2*3, 10/2)", - "icon": "tag", - "execute": function () {} - }) - } - return results - } - - // Regular app search - if (!query) { - results = results.concat(visibleEntries.sort(function (a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - })) - } else { - var fuzzyResults = Fuzzysort.go(query, visibleEntries, { - "keys": ["name", "comment", "genericName"] - }) - results = results.concat(fuzzyResults.map(function (r) { - return r.obj - })) - } - - Logger.log("AppLauncher", "Filtered entries:", results.length) - return results - } - Component.onCompleted: { Logger.log("AppLauncher", "Component completed") Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') @@ -302,7 +170,7 @@ NLoader { DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') } // Start clipboard refresh immediately on open - updateClipboardHistory() + clipboardHistory.refresh() } // Main content container @@ -368,7 +236,8 @@ NLoader { anchors.verticalCenter: parent.verticalCenter onTextChanged: { searchText = text - selectedIndex = 0 // Reset selection when search changes + // Defer selectedIndex reset to avoid binding loops + Qt.callLater(() => selectedIndex = 0) } selectedTextColor: Color.mOnSurface selectionColor: Color.mPrimary diff --git a/Modules/AppLauncher/Calculator.qml b/Modules/AppLauncher/Calculator.qml new file mode 100644 index 0000000..8675f0d --- /dev/null +++ b/Modules/AppLauncher/Calculator.qml @@ -0,0 +1,161 @@ +import QtQuick +import Quickshell +import qs.Commons + +import "../../Helpers/AdvancedMath.js" as AdvancedMath + +QtObject { + id: calculator + + // Function to evaluate mathematical expressions + function evaluate(expression) { + if (!expression || expression.trim() === "") { + return { + "isValid": false, + "result": "", + "displayResult": "", + "error": "Empty expression" + } + } + + try { + // Try advanced math first + if (typeof AdvancedMath !== 'undefined') { + const result = AdvancedMath.evaluate(expression.trim()) + const displayResult = AdvancedMath.formatResult(result) + + return { + "isValid": true, + "result": result, + "displayResult": displayResult, + "expression": expression, + "error": "" + } + } else { + // Fallback to basic evaluation + console.log("AdvancedMath not available, using basic eval") + + // Basic preprocessing for common functions + var processed = expression.trim() + .replace(/\bpi\b/gi, Math.PI) + .replace(/\be\b/gi, Math.E) + .replace(/\bsqrt\s*\(/g, 'Math.sqrt(') + .replace(/\bsin\s*\(/g, 'Math.sin(') + .replace(/\bcos\s*\(/g, 'Math.cos(') + .replace(/\btan\s*\(/g, 'Math.tan(') + .replace(/\blog\s*\(/g, 'Math.log10(') + .replace(/\bln\s*\(/g, 'Math.log(') + .replace(/\bexp\s*\(/g, 'Math.exp(') + .replace(/\bpow\s*\(/g, 'Math.pow(') + .replace(/\babs\s*\(/g, 'Math.abs(') + + // Sanitize and evaluate + if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) { + throw new Error("Invalid characters in expression") + } + + const result = eval(processed) + + if (!isFinite(result) || isNaN(result)) { + throw new Error("Invalid result") + } + + const displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '') + + return { + "isValid": true, + "result": result, + "displayResult": displayResult, + "expression": expression, + "error": "" + } + } + } catch (error) { + return { + "isValid": false, + "result": "", + "displayResult": "", + "error": error.message || error.toString() + } + } + } + + // Generate calculator entry for display + function createEntry(expression, searchContext = "") { + const evaluation = evaluate(expression) + + if (!evaluation.isValid) { + return { + "isCalculator": true, + "name": "Invalid expression", + "content": evaluation.error, + "icon": "error", + "execute": function () { + // Do nothing for invalid expressions + } + } + } + + const displayName = searchContext === "calc" + ? `${expression} = ${evaluation.displayResult}` + : `${expression} = ${evaluation.displayResult}` + + return { + "isCalculator": true, + "name": displayName, + "result": evaluation.result, + "expr": expression, + "displayResult": evaluation.displayResult, + "icon": "calculate", + "execute": function () { + Quickshell.clipboardText = evaluation.displayResult + // Also copy using shell command for better compatibility + Quickshell.execDetached(["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`]) + Quickshell.execDetached([ + "notify-send", + "Calculator", + `${expression} = ${evaluation.displayResult} (copied to clipboard)` + ]) + } + } + } + + // Create placeholder entry for empty calculator mode + function createPlaceholderEntry() { + return { + "isCalculator": true, + "name": "Calculator", + "content": "Try: sqrt(16), sin(1), cos(0), pi*2, exp(1), pow(2,8), abs(-5)", + "icon": "calculate", + "execute": function () { + // Do nothing for placeholder + } + } + } + + // Process calculator queries + function processQuery(query, searchContext = "") { + const results = [] + + if (searchContext === "calc") { + // Handle ">calc" mode + const expr = query.slice(5).trim() + if (expr && expr !== "") { + results.push(createEntry(expr, "calc")) + } else { + results.push(createPlaceholderEntry()) + } + } else if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) { + // Handle direct math expressions after ">" + const mathExpr = query.slice(1).trim() + const evaluation = evaluate(mathExpr) + + if (evaluation.isValid) { + results.push(createEntry(mathExpr, "direct")) + } + // If invalid, don't add anything - let it fall through to regular search + } + + return results + } +} diff --git a/Modules/AppLauncher/ClipboardHistory.qml b/Modules/AppLauncher/ClipboardHistory.qml new file mode 100644 index 0000000..dfd4d8c --- /dev/null +++ b/Modules/AppLauncher/ClipboardHistory.qml @@ -0,0 +1,158 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + +QtObject { + id: clipboardHistory + + // Copy helpers for different content types + function copyImageBase64(mime, base64) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + } + + function copyText(text) { + // Use printf with proper quoting to handle special characters + Quickshell.execDetached(["sh", "-c", `printf '%s' ${JSON.stringify(text)} | wl-copy -t text/plain`]) + } + + // Create clipboard entry for display + function createClipboardEntry(clip, index) { + if (clip.type === 'image') { + return { + "isClipboard": true, + "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + "content": "Image: " + clip.mimeType, + "icon": "image", + "type": 'image', + "data": clip.data, + "timestamp": clip.timestamp, + "index": index, + "execute": function () { + const dataParts = clip.data.split(',') + const base64Data = dataParts.length > 1 ? dataParts[1] : clip.data + copyImageBase64(clip.mimeType, base64Data) + Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) + } + } + } else { + // Handle text content + const textContent = clip.content || clip + let displayContent = textContent + let previewContent = "" + + // Normalize whitespace for display + displayContent = displayContent.replace(/\s+/g, ' ').trim() + + // Create preview for long content + if (displayContent.length > 50) { + previewContent = displayContent + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." + } + + return { + "isClipboard": true, + "name": displayContent, + "content": previewContent || textContent, + "icon": "content_paste", + "type": 'text', + "timestamp": clip.timestamp, + "index": index, + "textData": textContent, // Store the text data for the execute function + "execute": function () { + const text = this.textData || clip.content || clip + Quickshell.clipboardText = String(text) + copyText(String(text)) + var preview = (text.length > 50) ? text.slice(0, 50) + "…" : text + Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) + } + } + } + } + + // Create empty state entry + function createEmptyEntry() { + return { + "isClipboard": true, + "name": "No clipboard history", + "content": "No matching clipboard entries found", + "icon": "content_paste_off", + "execute": function () { + // Do nothing for empty state + } + } + } + + // Process clipboard queries + function processQuery(query) { + const results = [] + + if (!query.startsWith(">clip")) { + return results + } + + // Extract search term after ">clip " + const searchTerm = query.slice(5).trim() + + // Note: Clipboard refresh should be handled externally to avoid binding loops + + // Process each clipboard item + ClipboardService.history.forEach(function (clip, index) { + let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip + + // Apply search filter if provided + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm.toLowerCase())) { + const entry = createClipboardEntry(clip, index) + results.push(entry) + } + }) + + // Show empty state if no results + if (results.length === 0) { + results.push(createEmptyEntry()) + } + + return results + } + + // Create command entry for clipboard mode (deprecated - use direct creation in parent) + function createCommandEntry() { + return { + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": function () { + // This should be handled by the parent component + } + } + } + + // Utility function to refresh clipboard + function refresh() { + ClipboardService.refresh() + } + + // Get clipboard history count + function getHistoryCount() { + return ClipboardService.history ? ClipboardService.history.length : 0 + } + + // Get formatted timestamp for display + function formatTimestamp(timestamp) { + return new Date(timestamp).toLocaleTimeString() + } + + // Get clipboard entry by index + function getEntryByIndex(index) { + if (ClipboardService.history && index >= 0 && index < ClipboardService.history.length) { + return ClipboardService.history[index] + } + return null + } + + // Clear all clipboard history + function clearAll() { + ClipboardService.clearHistory() + } +} diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml index 008e83c..558b958 100644 --- a/Services/ClipboardService.qml +++ b/Services/ClipboardService.qml @@ -11,12 +11,40 @@ Singleton { property var history: [] property bool initialized: false + property int maxHistory: 50 // Limit clipboard history entries // Internal state property bool _enabled: true + // Cached history file path + property string historyFile: Quickshell.env("NOCTALIA_CLIPBOARD_HISTORY_FILE") + || (Settings.cacheDir + "clipboard.json") + + // Persisted storage for clipboard history + property FileView historyFileView: FileView { + id: historyFileView + objectName: "clipboardHistoryFileView" + path: historyFile + watchChanges: false // We don't need to watch changes for clipboard + onAdapterUpdated: writeAdapter() + Component.onCompleted: reload() + onLoaded: loadFromHistory() + onLoadFailed: function (error) { + // Create file on first use + if (error.toString().includes("No such file") || error === 2) { + writeAdapter() + } + } + + JsonAdapter { + id: historyAdapter + property var history: [] + property double timestamp: 0 + } + } + Timer { - interval: 1000 + interval: 2000 repeat: true running: root._enabled onTriggered: root.refresh() @@ -32,14 +60,17 @@ Singleton { if (exitCode === 0) { currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + // Always check for text first + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.isLoading = true + textProcess.running = true + + // Also check for images if available const imageType = currentTypes.find(t => t.startsWith('image/')) if (imageType) { imageProcess.mimeType = imageType imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] imageProcess.running = true - } else { - textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] - textProcess.running = true } } else { typeProcess.isLoading = false @@ -65,17 +96,31 @@ Singleton { "timestamp": new Date().getTime() } + // Check if this exact image already exists const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) if (!exists) { - root.history = [entry, ...root.history].slice(0, 20) + // Normalize existing history and add the new image + const normalizedHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime() - 1000 // Make it slightly older + } + } + return item + }) + root.history = [entry, ...normalizedHistory].slice(0, maxHistory) + saveHistory() } } } + // Always mark as initialized when done if (!textProcess.isLoading) { root.initialized = true + typeProcess.isLoading = false } - typeProcess.isLoading = false } stdout: StdioCollector {} @@ -87,15 +132,18 @@ Singleton { property bool isLoading: false onExited: (exitCode, exitStatus) => { + textProcess.isLoading = false + if (exitCode === 0) { const content = String(stdout.text).trim() - if (content) { + if (content && content.length > 0) { const entry = { "type": 'text', "content": content, "timestamp": new Date().getTime() } + // Check if this exact text content already exists const exists = root.history.find(item => { if (item.type === 'text') { return item.content === content @@ -104,36 +152,75 @@ Singleton { }) if (!exists) { - const newHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - "type": 'text', - "content": item, - "timestamp": new Date().getTime() - } - } - return item - }) + // Normalize existing history entries + const normalizedHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime() - 1000 // Make it slightly older + } + } + return item + }) - root.history = [entry, ...newHistory].slice(0, 20) + root.history = [entry, ...normalizedHistory].slice(0, maxHistory) + saveHistory() } } - } else { - textProcess.isLoading = false } + // Mark as initialized and clean up loading states root.initialized = true - typeProcess.isLoading = false + if (!imageProcess.running) { + typeProcess.isLoading = false + } } stdout: StdioCollector {} } function refresh() { - if (!typeProcess.isLoading && !textProcess.isLoading) { + if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) { typeProcess.isLoading = true typeProcess.command = ["wl-paste", "-l"] typeProcess.running = true } } + + function loadFromHistory() { + // Populate in-memory history from cached file + try { + const items = historyAdapter.history || [] + root.history = items.slice(0, maxHistory) // Apply limit when loading + Logger.log("Clipboard", "Loaded", root.history.length, "entries from cache") + } catch (e) { + Logger.error("Clipboard", "Failed to load history:", e) + root.history = [] + } + } + + function saveHistory() { + try { + // Ensure we don't exceed the maximum history limit + const limitedHistory = root.history.slice(0, maxHistory) + + historyAdapter.history = limitedHistory + historyAdapter.timestamp = Time.timestamp + + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) + + Qt.callLater(function () { + historyFileView.writeAdapter() + }) + } catch (e) { + Logger.error("Clipboard", "Failed to save history:", e) + } + } + + function clearHistory() { + root.history = [] + saveHistory() + } } From bb4510bbcdee3a3760c525494fdf12c4d25f0119 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 13:35:49 +0200 Subject: [PATCH 07/35] Add font settings, replace some Text with NText --- Commons/Settings.qml | 7 +++- Modules/AppLauncher/AppLauncher.qml | 2 +- Modules/LockScreen/LockScreen.qml | 46 +++++++++++------------ Modules/SettingsPanel/Tabs/GeneralTab.qml | 39 +++++++++++++++++++ Widgets/NCircleStat.qml | 3 +- Widgets/NText.qml | 2 +- 6 files changed, 72 insertions(+), 27 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6fb29dd..d7fec7d 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -223,7 +223,12 @@ Singleton { property JsonObject ui ui: JsonObject { - property string fontFamily: "Roboto" // Family for all text + property string fontDefault: "Roboto" // Default font for all text + property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal + property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays + + // Legacy compatibility + property string fontFamily: fontDefault // Keep for backward compatibility } // Scaling (not stored inside JsonObject, or it crashes) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 58f0c2c..502f95d 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -369,7 +369,7 @@ NLoader { visible: !parent.iconLoaded } - Text { + NText { anchors.centerIn: parent visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index aa1facf..161a6e3 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -233,13 +233,13 @@ NLoader { // Time display - Large and prominent with pulse animation Column { - spacing: Style.marginS * scaling + spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignHCenter - Text { + NText { id: timeText text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: "Inter" + font.family: Settings.data.ui.fontBillboard font.pointSize: Style.fontSizeXXXL * 6 * scaling font.weight: Font.Bold font.letterSpacing: -2 * scaling @@ -261,10 +261,10 @@ NLoader { } } - Text { + NText { id: dateText text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: "Inter" + font.family: Settings.data.ui.fontBillboard font.pointSize: Style.fontSizeXXL * scaling font.weight: Font.Light color: Color.mOnSurface @@ -404,10 +404,10 @@ NLoader { anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - Text { + NText { text: "SECURE TERMINAL" color: Color.mOnSurface - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling font.weight: Font.Bold Layout.fillWidth: true @@ -424,10 +424,10 @@ NLoader { color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface } - Text { + NText { text: Math.round(batteryIndicator.percent) + "%" color: Color.mOnSurface - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling font.weight: Font.Bold } @@ -450,19 +450,19 @@ NLoader { Layout.fillWidth: true spacing: Style.marginM * scaling - Text { + NText { text: "root@noctalia:~$" color: Color.mPrimary - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling font.weight: Font.Bold } - Text { + NText { id: welcomeText text: "" color: Color.mOnSurface - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling property int currentIndex: 0 property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" @@ -488,18 +488,18 @@ NLoader { Layout.fillWidth: true spacing: Style.marginM * scaling - Text { + NText { text: "root@noctalia:~$" color: Color.mPrimary - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling font.weight: Font.Bold } - Text { + NText { text: "sudo unlock-session" color: Color.mOnSurface - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling } @@ -509,7 +509,7 @@ NLoader { width: 0 height: 0 visible: false - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling color: Color.mOnSurface echoMode: TextInput.Password @@ -535,11 +535,11 @@ NLoader { } // Visual password display with integrated cursor - Text { + NText { id: asterisksText text: "*".repeat(passwordInput.text.length) color: Color.mOnSurface - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling visible: passwordInput.activeFocus @@ -585,7 +585,7 @@ NLoader { } // Status messages - Text { + NText { text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) font.family: "DejaVu Sans Mono" @@ -618,11 +618,11 @@ NLoader { Layout.alignment: Qt.AlignRight Layout.bottomMargin: -12 * scaling - Text { + NText { anchors.centerIn: parent text: lock.authenticating ? "EXECUTING" : "EXECUTE" color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - font.family: "DejaVu Sans Mono" + font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling font.weight: Font.Bold } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index ae5f76f..40af08b 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -92,6 +92,45 @@ ColumnLayout { Layout.bottomMargin: Style.marginS * scaling } + // Font configuration section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NTextInput { + label: "Default Font" + description: "Main font used throughout the interface." + text: Settings.data.ui.fontDefault + placeholderText: "Roboto" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontDefault = text + } + } + + NTextInput { + label: "Fixed Width Font" + description: "Monospace font used for terminal and code display." + text: Settings.data.ui.fontFixed + placeholderText: "DejaVu Sans Mono" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontFixed = text + } + } + + NTextInput { + label: "Billboard Font" + description: "Large font used for clocks and prominent displays." + text: Settings.data.ui.fontBillboard + placeholderText: "Inter" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontBillboard = text + } + } + } + NToggle { label: "Show Corners" description: "Display rounded corners on the edge of the screen." diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index e7d7d1a..1bd9e67 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,6 +1,7 @@ import QtQuick import qs.Commons import qs.Services +import qs.Widgets // Compact circular statistic display used in the SidePanel Rectangle { @@ -73,7 +74,7 @@ Rectangle { } // Percent centered in the circle - Text { + NText { id: valueLabel anchors.centerIn: parent text: `${root.value}${root.suffix}` diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 59d7d63..7436c54 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -6,7 +6,7 @@ import qs.Widgets Text { id: root - font.family: Settings.data.ui.fontFamily + font.family: Settings.data.ui.fontDefault font.pointSize: Style.fontSizeM * scaling font.weight: Style.fontWeightMedium color: Color.mOnSurface From bfc2adfca4c973bc8e664453a7b7d4a53fbd00e1 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 13:40:42 +0200 Subject: [PATCH 08/35] Move Font Settings into own category --- Modules/SettingsPanel/Tabs/GeneralTab.qml | 92 +++++++++++++---------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 40af08b..8da9077 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -92,45 +92,6 @@ ColumnLayout { Layout.bottomMargin: Style.marginS * scaling } - // Font configuration section - ColumnLayout { - spacing: Style.marginS * scaling - Layout.fillWidth: true - - NTextInput { - label: "Default Font" - description: "Main font used throughout the interface." - text: Settings.data.ui.fontDefault - placeholderText: "Roboto" - Layout.fillWidth: true - onEditingFinished: { - Settings.data.ui.fontDefault = text - } - } - - NTextInput { - label: "Fixed Width Font" - description: "Monospace font used for terminal and code display." - text: Settings.data.ui.fontFixed - placeholderText: "DejaVu Sans Mono" - Layout.fillWidth: true - onEditingFinished: { - Settings.data.ui.fontFixed = text - } - } - - NTextInput { - label: "Billboard Font" - description: "Large font used for clocks and prominent displays." - text: Settings.data.ui.fontBillboard - placeholderText: "Inter" - Layout.fillWidth: true - onEditingFinished: { - Settings.data.ui.fontBillboard = text - } - } - } - NToggle { label: "Show Corners" description: "Display rounded corners on the edge of the screen." @@ -196,6 +157,59 @@ ColumnLayout { } } } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginL * 2 * scaling + Layout.bottomMargin: Style.marginL * scaling + } + + NText { + text: "Fonts" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginS * scaling + } + + // Font configuration section + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + + NTextInput { + label: "Default Font" + description: "Main font used throughout the interface." + text: Settings.data.ui.fontDefault + placeholderText: "Roboto" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontDefault = text + } + } + + NTextInput { + label: "Fixed Width Font" + description: "Monospace font used for terminal and code display." + text: Settings.data.ui.fontFixed + placeholderText: "DejaVu Sans Mono" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontFixed = text + } + } + + NTextInput { + label: "Billboard Font" + description: "Large font used for clocks and prominent displays." + text: Settings.data.ui.fontBillboard + placeholderText: "Inter" + Layout.fillWidth: true + onEditingFinished: { + Settings.data.ui.fontBillboard = text + } + } + } } } } From e7577ed4883e7b75e798d7e18e03f40eebfb37ab Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 14:22:29 +0200 Subject: [PATCH 09/35] Add IdleInhibitor --- Commons/Settings.qml | 3 + Modules/IPC/IPCManager.qml | 3 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 11 ++ Services/IdleInhibitorService.qml | 209 ++++++++++++++++++++++ Services/MediaService.qml | 5 + shell.qml | 1 + 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 Services/IdleInhibitorService.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d7fec7d..3b9466a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -229,6 +229,9 @@ Singleton { // Legacy compatibility property string fontFamily: fontDefault // Keep for backward compatibility + + // Idle inhibitor state + property bool idleInhibitorEnabled: false } // Scaling (not stored inside JsonObject, or it crashes) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 638bc8e..6eb0d50 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -27,7 +27,8 @@ Item { IpcHandler { target: "idleInhibitor" - function toggle() {// TODO + function toggle() { + return IdleInhibitorService.manualToggle() } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9bc0739..1261c49 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -31,6 +31,17 @@ NBox { } } + // Idle Inhibitor + NIconButton { + icon: IdleInhibitorService.isInhibited ? "coffee" : "bedtime" + tooltipText: IdleInhibitorService.isInhibited ? "Disable Keep Awake" : "Enable Keep Awake" + colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant + colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary + onClicked: { + IdleInhibitorService.manualToggle() + } + } + // Wallpaper NIconButton { icon: "image" diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml new file mode 100644 index 0000000..bece9db --- /dev/null +++ b/Services/IdleInhibitorService.qml @@ -0,0 +1,209 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services + +Singleton { + id: root + + property bool isInhibited: false + property string reason: "User requested" + property var activeInhibitors: [] + + // Different inhibitor strategies + property string strategy: "systemd" // "systemd", "wayland", or "auto" + + Component.onCompleted: { + Logger.log("IdleInhibitor", "Service started") + detectStrategy() + + // Restore previous state from settings + if (Settings.data.ui.idleInhibitorEnabled) { + addInhibitor("manual", "Restored from previous session") + Logger.log("IdleInhibitor", "Restored previous manual inhibition state") + } + } + + // Auto-detect the best strategy + function detectStrategy() { + if (strategy === "auto") { + // Check if systemd-inhibit is available + try { + var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"]) + strategy = "systemd" + Logger.log("IdleInhibitor", "Using systemd-inhibit strategy") + return + } catch (e) { + // systemd-inhibit not found, try Wayland tools + } + + try { + var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]) + strategy = "wayland" + Logger.log("IdleInhibitor", "Using wayhibitor strategy") + return + } catch (e) { + // wayhibitor not found + } + + Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback") + strategy = "systemd" // Fallback to systemd even if not detected + } + } + + // Add an inhibitor + function addInhibitor(id, reason = "Application request") { + if (activeInhibitors.includes(id)) { + Logger.warn("IdleInhibitor", "Inhibitor already active:", id) + return false + } + + activeInhibitors.push(id) + updateInhibition(reason) + Logger.log("IdleInhibitor", "Added inhibitor:", id) + return true + } + + // Remove an inhibitor + function removeInhibitor(id) { + const index = activeInhibitors.indexOf(id) + if (index === -1) { + Logger.warn("IdleInhibitor", "Inhibitor not found:", id) + return false + } + + activeInhibitors.splice(index, 1) + updateInhibition() + Logger.log("IdleInhibitor", "Removed inhibitor:", id) + return true + } + + // Update the actual system inhibition + function updateInhibition(newReason = reason) { + const shouldInhibit = activeInhibitors.length > 0 + + if (shouldInhibit === isInhibited) { + return // No change needed + } + + if (shouldInhibit) { + startInhibition(newReason) + } else { + stopInhibition() + } + } + + // Start system inhibition + function startInhibition(newReason) { + reason = newReason + + if (strategy === "systemd") { + startSystemdInhibition() + } else if (strategy === "wayland") { + startWaylandInhibition() + } else { + Logger.warn("IdleInhibitor", "No inhibition strategy available") + return + } + + isInhibited = true + Logger.log("IdleInhibitor", "Started inhibition:", reason) + } + + // Stop system inhibition + function stopInhibition() { + if (!isInhibited) return + + if (inhibitorProcess.running) { + inhibitorProcess.signal(15) // SIGTERM + } + + isInhibited = false + Logger.log("IdleInhibitor", "Stopped inhibition") + } + + // Systemd inhibition using systemd-inhibit + function startSystemdInhibition() { + inhibitorProcess.command = [ + "systemd-inhibit", + "--what=idle:sleep:handle-lid-switch", + "--why=" + reason, + "--mode=block", + "sleep", "infinity" + ] + inhibitorProcess.running = true + } + + // Wayland inhibition using wayhibitor or similar + function startWaylandInhibition() { + inhibitorProcess.command = ["wayhibitor"] + inhibitorProcess.running = true + } + + // Process for maintaining the inhibition + Process { + id: inhibitorProcess + running: false + + onExited: function(exitCode, exitStatus) { + if (isInhibited) { + Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode) + isInhibited = false + } + } + + onStarted: function() { + Logger.log("IdleInhibitor", "Inhibitor process started successfully") + } + } + + // Convenience functions for common use cases + function inhibitForMedia(active = true) { + if (active) { + addInhibitor("media", "Media playback active") + } else { + removeInhibitor("media") + } + } + + function inhibitForPresentation(active = true) { + if (active) { + addInhibitor("presentation", "Presentation mode") + } else { + removeInhibitor("presentation") + } + } + + function inhibitForFullscreen(active = true) { + if (active) { + addInhibitor("fullscreen", "Fullscreen application") + } else { + removeInhibitor("fullscreen") + } + } + + // Manual toggle for user control + function manualToggle() { + if (activeInhibitors.includes("manual")) { + removeInhibitor("manual") + Settings.data.ui.idleInhibitorEnabled = false + ToastService.showNotice("Keep Awake", "Disabled", false, 3000) + Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings") + return false + } else { + addInhibitor("manual", "Manually activated by user") + Settings.data.ui.idleInhibitorEnabled = true + ToastService.showNotice("Keep Awake", "Enabled", false, 3000) + Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings") + return true + } + } + + // Clean up on shutdown + Component.onDestruction: { + stopInhibition() + } +} diff --git a/Services/MediaService.qml b/Services/MediaService.qml index c44738d..b4434a2 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -13,6 +13,11 @@ Singleton { property real currentPosition: 0 property int selectedPlayerIndex: 0 property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + + // Auto-inhibit idle when media is playing + onIsPlayingChanged: { + IdleInhibitorService.inhibitForMedia(isPlaying) + } property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" diff --git a/shell.qml b/shell.qml index de85aee..c4bf5e9 100644 --- a/shell.qml +++ b/shell.qml @@ -25,6 +25,7 @@ import qs.Modules.Notification import qs.Modules.SettingsPanel import qs.Modules.SidePanel import qs.Modules.Toast + import qs.Services import qs.Widgets From d3add14ce8e2f149a0e6f37103e1f950bbe1d418 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 14:27:17 +0200 Subject: [PATCH 10/35] Fix Lockscreen (#121) --- Modules/IPC/IPCManager.qml | 6 +++++- Modules/LockScreen/LockScreen.qml | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 6eb0d50..28107c2 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -44,7 +44,11 @@ Item { target: "lockScreen" function toggle() { - lockScreen.isLoaded = !lockScreen.isLoaded + // Only lock if not already locked (prevents the red screen issue) + // Note: No unlock via IPC for security reasons + if (!lockScreen.isLoaded) { + lockScreen.isLoaded = true + } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 161a6e3..f800318 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -14,14 +14,24 @@ import qs.Widgets NLoader { id: lockScreen + + // Log state changes to help debug lock screen issues + onIsLoadedChanged: { + Logger.log("LockScreen", "State changed - isLoaded:", isLoaded) + } + // Allow a small grace period after unlocking so the compositor releases the lock surfaces Timer { id: unloadAfterUnlockTimer interval: 250 repeat: false - onTriggered: lockScreen.isLoaded = false + onTriggered: { + Logger.log("LockScreen", "Unload timer triggered - setting isLoaded to false") + lockScreen.isLoaded = false + } } function scheduleUnloadAfterUnlock() { + Logger.log("LockScreen", "Scheduling unload after unlock") unloadAfterUnlockTimer.start() } content: Component { From 73b0dfa0fff4b25ab17c794022b14ad1d4adf01c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 14:34:05 +0200 Subject: [PATCH 11/35] Add namespace for Bar & NPanel (noctalia-bar, noctalia-panel) --- Modules/Bar/Bar.qml | 3 +++ Widgets/NPanel.qml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e2e4beb..2805af4 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets @@ -17,6 +18,8 @@ Variants { readonly property real scaling: ScalingService.scale(screen) screen: modelData + WlrLayershell.namespace: "noctalia-bar" + implicitHeight: Style.barHeight * scaling color: Color.transparent diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8b8efe5..351e437 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -7,6 +7,8 @@ import qs.Services PanelWindow { id: root + WlrLayershell.namespace: "noctalia-panel" + readonly property real scaling: ScalingService.scale(screen) property bool showOverlay: Settings.data.general.dimDesktop From 07705bb59cd096fd145e6dc880b2a1ac1daaad9f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 14:41:52 +0200 Subject: [PATCH 12/35] Remove auto idle inhibit when media is playing --- Services/IdleInhibitorService.qml | 25 ------------------------- Services/MediaService.qml | 5 ----- 2 files changed, 30 deletions(-) diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml index bece9db..a37a452 100644 --- a/Services/IdleInhibitorService.qml +++ b/Services/IdleInhibitorService.qml @@ -160,31 +160,6 @@ Singleton { } } - // Convenience functions for common use cases - function inhibitForMedia(active = true) { - if (active) { - addInhibitor("media", "Media playback active") - } else { - removeInhibitor("media") - } - } - - function inhibitForPresentation(active = true) { - if (active) { - addInhibitor("presentation", "Presentation mode") - } else { - removeInhibitor("presentation") - } - } - - function inhibitForFullscreen(active = true) { - if (active) { - addInhibitor("fullscreen", "Fullscreen application") - } else { - removeInhibitor("fullscreen") - } - } - // Manual toggle for user control function manualToggle() { if (activeInhibitors.includes("manual")) { diff --git a/Services/MediaService.qml b/Services/MediaService.qml index b4434a2..c44738d 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -13,11 +13,6 @@ Singleton { property real currentPosition: 0 property int selectedPlayerIndex: 0 property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false - - // Auto-inhibit idle when media is playing - onIsPlayingChanged: { - IdleInhibitorService.inhibitForMedia(isPlaying) - } property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" From 57fee687938af9a1dd51a7c30a0f043bf24efcbb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 08:45:48 -0400 Subject: [PATCH 13/35] NPanel refactor - 1st pass: SidePanel and settings an new logic --- Commons/Settings.qml | 9 +- Modules/Bar/SidePanelToggle.qml | 37 +- Modules/Bar/Tray.qml | 1 - Modules/Bar/Volume.qml | 2 +- Modules/SettingsPanel/SettingsPanel.qml | 550 ++++++++---------- Modules/SidePanel/Cards/PowerProfilesCard.qml | 3 +- Modules/SidePanel/Cards/ProfileCard.qml | 7 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 7 +- Modules/SidePanel/SidePanel.qml | 242 ++------ Services/CavaService.qml | 2 +- Services/PanelService.qml | 10 +- Services/ScalingService.qml | 10 +- Widgets/NLoader.qml | 5 +- Widgets/NPanel.qml | 231 +++++--- 14 files changed, 500 insertions(+), 616 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6fb29dd..1f5d45c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -30,9 +30,6 @@ Singleton { // Flag to prevent unnecessary wallpaper calls during reloads property bool isInitialLoad: true - // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN - //property var openPanel: null - // Function to validate monitor configurations function validateMonitorConfigurations() { var availableScreenNames = [] @@ -86,14 +83,14 @@ Singleton { if (isInitialLoad) { Logger.log("Settings", "OnLoaded") // Only set wallpaper on initial load, not on reloads - if (adapter.wallpaper.current !== "") { + if (adapter.wallpaper.current !== "") { Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) } // Validate monitor configurations, only once // if none of the configured monitors exist, clear the lists - validateMonitorConfigurations() + validateMonitorConfigurations() } isInitialLoad = false @@ -128,7 +125,7 @@ Singleton { general: JsonObject { property string avatarImage: defaultAvatar - property bool dimDesktop: true + property bool dimDesktop: false property bool showScreenCorners: false property real radiusRatio: 1.0 } diff --git a/Modules/Bar/SidePanelToggle.qml b/Modules/Bar/SidePanelToggle.qml index 4fcb347..542c5b2 100644 --- a/Modules/Bar/SidePanelToggle.qml +++ b/Modules/Bar/SidePanelToggle.qml @@ -15,22 +15,25 @@ NIconButton { anchors.verticalCenter: parent.verticalCenter onClicked: { - // Map this button's center to the screen and open the side panel below it - const localCenterX = width / 2 - const localCenterY = height / 2 - const globalPoint = mapToItem(null, localCenterX, localCenterY) - if (sidePanel.isLoaded) { - // Call hide() instead of directly setting isLoaded to false - if (sidePanel.item && sidePanel.item.hide) { - sidePanel.item.hide() - } else { - sidePanel.isLoaded = false - } - } else if (sidePanel.openAt) { - sidePanel.openAt(globalPoint.x, screen) - } else { - // Fallback: toggle if API unavailable - sidePanel.isLoaded = true - } + sidePanel.toggle(screen) + // sidePanel.isLoaded = !sidePanel.isLoaded + // Logger.log("SidePanelToggle", sidePanel.isLoaded) + // // Map this button's center to the screen and open the side panel below it + // const localCenterX = width / 2 + // const localCenterY = height / 2 + // const globalPoint = mapToItem(null, localCenterX, localCenterY) + // if (sidePanel.isLoaded) { + // // Call hide() instead of directly setting isLoaded to false + // if (sidePanel.item && sidePanel.item.hide) { + // sidePanel.item.hide() + // } else { + // sidePanel.isLoaded = false + // } + // } else if (sidePanel.openAt) { + // sidePanel.openAt(globalPoint.x, screen) + // } else { + // // Fallback: toggle if API unavailable + // sidePanel.isLoaded = true + // } } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 6718dd8..85f79c3 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -129,7 +129,6 @@ Rectangle { // Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu NPanel { id: trayPanel - showOverlay: false // no colors overlay even if activated in settings // Override hide function to animate first function hide() { diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 8eb6196..36dbbb1 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -65,7 +65,7 @@ Item { } onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.AudioService - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 7012838..b4b5ff4 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -8,9 +8,13 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +NPanel { id: root + rWidth: Math.max(screen?.width * 0.5, 1280) * scaling + rHeight: Math.max(screen?.height * 0.5, 720) * scaling + rAnchorCentered: true + // Tabs enumeration, order is NOT relevant enum Tab { About, @@ -28,344 +32,264 @@ NLoader { } property int requestedTab: SettingsPanel.Tab.General + property int currentTabIndex: 0 - content: Component { - NPanel { - id: panel + Component { + id: generalTab + Tabs.GeneralTab {} + } + Component { + id: barTab + Tabs.BarTab {} + } + Component { + id: audioTab + Tabs.AudioTab {} + } + Component { + id: brightnessTab + Tabs.BrightnessTab {} + } + Component { + id: displayTab + Tabs.DisplayTab {} + } + Component { + id: networkTab + Tabs.NetworkTab {} + } + Component { + id: timeWeatherTab + Tabs.TimeWeatherTab {} + } + Component { + id: colorSchemeTab + Tabs.ColorSchemeTab {} + } + Component { + id: wallpaperTab + Tabs.WallpaperTab {} + } + Component { + id: wallpaperSelectorTab + Tabs.WallpaperSelectorTab {} + } + Component { + id: screenRecorderTab + Tabs.ScreenRecorderTab {} + } + Component { + id: aboutTab + Tabs.AboutTab {} + } - property int currentTabIndex: 0 + // Order *DOES* matter + property var tabsModel: [{ + "id": SettingsPanel.Tab.General, + "label": "General", + "icon": "tune", + "source": generalTab + }, { + "id": SettingsPanel.Tab.Bar, + "label": "Bar", + "icon": "web_asset", + "source": barTab + }, { + "id": SettingsPanel.Tab.AudioService, + "label": "Audio", + "icon": "volume_up", + "source": audioTab + }, { + "id": SettingsPanel.Tab.Display, + "label": "Display", + "icon": "monitor", + "source": displayTab + }, { + "id": SettingsPanel.Tab.Network, + "label": "Network", + "icon": "lan", + "source": networkTab + }, { + "id": SettingsPanel.Tab.Brightness, + "label": "Brightness", + "icon": "brightness_6", + "source": brightnessTab + }, { + "id": SettingsPanel.Tab.TimeWeather, + "label": "Time & Weather", + "icon": "schedule", + "source": timeWeatherTab + }, { + "id": SettingsPanel.Tab.ColorScheme, + "label": "Color Scheme", + "icon": "palette", + "source": colorSchemeTab + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "Wallpaper", + "icon": "image", + "source": wallpaperTab + }, { + "id": SettingsPanel.Tab.WallpaperSelector, + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow", + "source": wallpaperSelectorTab + }, { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "Screen Recorder", + "icon": "videocam", + "source": screenRecorderTab + }, { + "id": SettingsPanel.Tab.About, + "label": "About", + "icon": "info", + "source": aboutTab + }] - // Override hide function to animate first - function hide() { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - // Hide after animation completes - hideTimer.start() - } - - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: panel - function onDismissed() { - hide() + // When the panel opens, choose the appropriate tab + onOpened: { + var initialIndex = SettingsPanel.Tab.General + if (root.requestedTab !== null) { + for (var i = 0; i < root.tabsModel.length; i++) { + if (root.tabsModel[i].id === root.requestedTab) { + initialIndex = i + break } } + } + // Now that the UI is settled, set the current tab index. + root.currentTabIndex = initialIndex + } - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - panel.visible = false - panel.dismissed() - } - } + panelContent: Rectangle { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + color: Color.transparent - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Component { - id: generalTab - Tabs.GeneralTab {} - } - Component { - id: barTab - Tabs.BarTab {} - } - Component { - id: audioTab - Tabs.AudioTab {} - } - Component { - id: brightnessTab - Tabs.BrightnessTab {} - } - Component { - id: displayTab - Tabs.DisplayTab {} - } - Component { - id: networkTab - Tabs.NetworkTab {} - } - Component { - id: timeWeatherTab - Tabs.TimeWeatherTab {} - } - Component { - id: colorSchemeTab - Tabs.ColorSchemeTab {} - } - Component { - id: wallpaperTab - Tabs.WallpaperTab {} - } - Component { - id: wallpaperSelectorTab - Tabs.WallpaperSelectorTab {} - } - Component { - id: screenRecorderTab - Tabs.ScreenRecorderTab {} - } - Component { - id: aboutTab - Tabs.AboutTab {} - } - - // Order *DOES* matter - property var tabsModel: [{ - "id": SettingsPanel.Tab.General, - "label": "General", - "icon": "tune", - "source": generalTab - }, { - "id": SettingsPanel.Tab.Bar, - "label": "Bar", - "icon": "web_asset", - "source": barTab - }, { - "id": SettingsPanel.Tab.AudioService, - "label": "Audio", - "icon": "volume_up", - "source": audioTab - }, { - "id": SettingsPanel.Tab.Display, - "label": "Display", - "icon": "monitor", - "source": displayTab - }, { - "id": SettingsPanel.Tab.Network, - "label": "Network", - "icon": "lan", - "source": networkTab - }, { - "id": SettingsPanel.Tab.Brightness, - "label": "Brightness", - "icon": "brightness_6", - "source": brightnessTab - }, { - "id": SettingsPanel.Tab.TimeWeather, - "label": "Time & Weather", - "icon": "schedule", - "source": timeWeatherTab - }, { - "id": SettingsPanel.Tab.ColorScheme, - "label": "Color Scheme", - "icon": "palette", - "source": colorSchemeTab - }, { - "id": SettingsPanel.Tab.Wallpaper, - "label": "Wallpaper", - "icon": "image", - "source": wallpaperTab - }, { - "id": SettingsPanel.Tab.WallpaperSelector, - "label": "Wallpaper Selector", - "icon": "wallpaper_slideshow", - "source": wallpaperSelectorTab - }, { - "id": SettingsPanel.Tab.ScreenRecorder, - "label": "Screen Recorder", - "icon": "videocam", - "source": screenRecorderTab - }, { - "id": SettingsPanel.Tab.About, - "label": "About", - "icon": "info", - "source": aboutTab - }] - - Component.onCompleted: { - var initialIndex = 0 - if (root.requestedTab !== null) { - for (var i = 0; i < panel.tabsModel.length; i++) { - if (panel.tabsModel[i].id === root.requestedTab) { - initialIndex = i - break - } - } - } - // Now that the UI is settled, set the current tab index. - panel.currentTabIndex = initialIndex - show() - } - - onVisibleChanged: { - if (!visible && (bgRect.opacityValue > 0)) { - hide() - } - } + RowLayout { + anchors.fill: parent + spacing: Style.marginM * scaling Rectangle { - id: bgRect - color: Color.mSurface - radius: Style.radiusL * scaling + id: sidebar + Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling + Layout.fillHeight: true + color: Color.mSurfaceVariant border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - layer.enabled: true - width: Math.max(screen.width * 0.5, 1280) * scaling - height: Math.max(screen.height * 0.5, 720) * scaling - anchors.centerIn: parent + radius: Style.radiusM * scaling - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - MouseArea { + Column { anchors.fill: parent - } + anchors.margins: Style.marginS * scaling + spacing: Style.marginXS * 1.5 * scaling - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling - - Rectangle { - id: sidebar - Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling - Layout.fillHeight: true - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling - - Column { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling - - Repeater { - id: sections - model: panel.tabsModel - delegate: Rectangle { - id: tabItem - width: parent.width - height: 32 * scaling - radius: Style.radiusS * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) - readonly property bool selected: index === currentTabIndex - property bool hovering: false - property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling - spacing: Style.marginS * scaling - // Tab icon on the left side - NIcon { - text: modelData.icon - color: tabTextColor - font.pointSize: Style.fontSizeL * scaling - } - // Tab label on the left side - NText { - text: modelData.label - color: tabTextColor - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - onEntered: tabItem.hovering = true - onExited: tabItem.hovering = false - onCanceled: tabItem.hovering = false - onClicked: currentTabIndex = index - } + Repeater { + id: sections + model: root.tabsModel + delegate: Rectangle { + id: tabItem + width: parent.width + height: 32 * scaling + radius: Style.radiusS * scaling + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) + readonly property bool selected: index === currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling + // Tab icon on the left side + NIcon { + text: modelData.icon + color: tabTextColor + font.pointSize: Style.fontSizeL * scaling } + // Tab label on the left side + NText { + text: modelData.label + color: tabTextColor + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: currentTabIndex = index } } } + } + } - // Content - Rectangle { - id: contentPane + // Content + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusM * scaling + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + clip: true + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling + + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginS * scaling + + // Tab label on the main right side + NText { + text: root.tabsModel[currentTabIndex].label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + NIconButton { + icon: "close" + tooltipText: "Close" + Layout.alignment: Qt.AlignVCenter + onClicked: root.close() + } + } + + NDivider { + Layout.fillWidth: true + } + + Item { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) clip: true - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling + Repeater { + model: root.tabsModel - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS * scaling - - // Tab label on the main right side - NText { - text: panel.tabsModel[currentTabIndex].label - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - } - NIconButton { - icon: "close" - tooltipText: "Close" - Layout.alignment: Qt.AlignVCenter - onClicked: panel.hide() - } + onItemAdded: function (index, item) { + item.sourceComponent = root.tabsModel[index].source } - NDivider { - Layout.fillWidth: true - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - Repeater { - model: panel.tabsModel - - onItemAdded: function (index, item) { - item.sourceComponent = panel.tabsModel[index].source - } - - delegate: Loader { - // All loaders will occupy the same space, stacked on top of each other. - anchors.fill: parent - visible: index === panel.currentTabIndex - // The loader is only active (and uses memory) when its page is visible. - active: visible - } - } + delegate: Loader { + // All loaders will occupy the same space, stacked on top of each other. + anchors.fill: parent + visible: index === root.currentTabIndex + // The loader is only active (and uses memory) when its page is visible. + active: visible } } } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 4c574b0..2c36642 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -16,12 +16,13 @@ NBox { // PowerProfiles service property var powerProfiles: PowerProfiles readonly property bool hasPP: powerProfiles.hasPerformanceProfile + property real spacing: 0 RowLayout { id: powerRow anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: sidePanel.cardSpacing + spacing: spacing Item { Layout.fillWidth: true } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 1b39336..803565a 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -61,7 +61,7 @@ NBox { tooltipText: "Open Settings" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General - settingsPanel.isLoaded = !settingsPanel.isLoaded + settingsPanel.open(screen) } } @@ -78,8 +78,9 @@ NBox { PowerMenu { id: powerMenu - anchors.top: powerButton.bottom - anchors.right: powerButton.right + // TBC + // anchors.top: powerButton.bottom + // anchors.right: powerButton.right } // ---------------------------------- diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9bc0739..2037ebb 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -9,6 +9,9 @@ import qs.Widgets // Utilities: record & wallpaper NBox { + + property real spacing: 0 + Layout.fillWidth: true Layout.preferredWidth: 1 implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling @@ -16,7 +19,7 @@ NBox { id: utilRow anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: sidePanel.cardSpacing + spacing: spacing Item { Layout.fillWidth: true } @@ -37,7 +40,7 @@ NBox { tooltipText: "Open Wallpaper Selector" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 04bfa3f..58b3065 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -7,205 +7,79 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { - id: root +NPanel { + id: panel - // X coordinate on screen (in pixels) where the panel should align its center. - // Set via openAt(x) from the bar button. - property real anchorX: 0 - // Target screen to open on - property var targetScreen: null + rWidth: 460 * scaling + rHeight: 700 * scaling + rAnchorRight: true - function openAt(x, screen) { - anchorX = x - targetScreen = screen - isLoaded = true - // If the panel is already instantiated, update immediately - if (item) { - if (item.anchorX !== undefined) - item.anchorX = anchorX - if (item.screen !== undefined) - item.screen = targetScreen - } - } + // rectX: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, + // Math.round(anchorX - width / 2))) + // rectY: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined + panelContent: Item { + id: content - content: Component { - NPanel { - id: sidePanel + property real cardSpacing: Style.marginL * scaling - // Single source of truth for spacing between cards (both axes) - property real cardSpacing: Style.marginL * scaling - // X coordinate from the bar to align this panel under - property real anchorX: root.anchorX - // Ensure this panel attaches to the intended screen - screen: root.targetScreen + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: content.cardSpacing + implicitHeight: layout.implicitHeight - // Override hide function to animate first - function hide() { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Layout content (not vertically anchored so implicitHeight is valid) + ColumnLayout { + id: layout + // Use the same spacing value horizontally and vertically + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() + // Cards (consistent inter-card spacing via ColumnLayout spacing) + ProfileCard {// Layout.topMargin: 0 + // Layout.bottomMargin: 0 + } + WeatherCard {// Layout.topMargin: 0 + // Layout.bottomMargin: 0 } - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: sidePanel - function onDismissed() { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Middle section: media + stats column + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() + // Media card + MediaCard { + id: mediaCard + Layout.fillWidth: true + implicitHeight: statsCard.implicitHeight + } + + // System monitors combined in one card + SystemMonitorCard { + id: statsCard } } - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && panelBackground.opacityValue > 0) { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Bottom actions (two grouped rows of round buttons) + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() - } - } - - // Ensure panel shows itself once created - Component.onCompleted: show() - - // Inline helpers moved to dedicated widgets: NCard and NCircleStat - Rectangle { - id: panelBackground - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - layer.enabled: true - width: 460 * scaling - property real innerMargin: sidePanel.cardSpacing - // Height scales to content plus vertical padding - height: content.implicitHeight + innerMargin * 2 - // Place the panel relative to the bar based on its position - y: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined - anchors { - bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined - bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginS * scaling : undefined - } - // Center horizontally under the anchorX, clamped to the screen bounds - x: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, - Math.round(anchorX - width / 2))) - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 + // Power Profiles switcher + PowerProfilesCard { + spacing: content.cardSpacing } - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - sidePanel.visible = false - sidePanel.dismissed() - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - // Content wrapper to ensure childrenRect drives implicit height - Item { - id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: panelBackground.innerMargin - implicitHeight: layout.implicitHeight - - // Layout content (not vertically anchored so implicitHeight is valid) - ColumnLayout { - id: layout - // Use the same spacing value horizontally and vertically - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: sidePanel.cardSpacing - - // Cards (consistent inter-card spacing via ColumnLayout spacing) - ProfileCard { - Layout.topMargin: 0 - Layout.bottomMargin: 0 - } - WeatherCard { - Layout.topMargin: 0 - Layout.bottomMargin: 0 - } - - // Middle section: media + stats column - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 - spacing: sidePanel.cardSpacing - - // Media card - MediaCard { - id: mediaCard - Layout.fillWidth: true - implicitHeight: statsCard.implicitHeight - } - - // System monitors combined in one card - SystemMonitorCard { - id: statsCard - } - } - - // Bottom actions (two grouped rows of round buttons) - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 - spacing: sidePanel.cardSpacing - - // Power Profiles switcher - PowerProfilesCard {} - - // Utilities buttons - UtilitiesCard {} - } - } + // Utilities buttons + UtilitiesCard { + spacing: content.cardSpacing } } } diff --git a/Services/CavaService.qml b/Services/CavaService.qml index b58e09a..dae2f61 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -37,7 +37,7 @@ Singleton { Process { id: process stdinEnabled: true - running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.isLoaded + running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.active command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 8f0ca3a..940e242 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -5,8 +5,16 @@ import Quickshell Singleton { id: root + // A ref. to the sidePanel, so it's accessible from other services + property var sidePanel: null + // Currently opened panel property var openedPanel: null - property var sidePanel: null + function registerOpen(panel) { + if (openedPanel && openedPanel != panel) { + openedPanel.close() + } + openedPanel = panel + } } diff --git a/Services/ScalingService.qml b/Services/ScalingService.qml index 9679bc5..2a9792d 100644 --- a/Services/ScalingService.qml +++ b/Services/ScalingService.qml @@ -9,7 +9,15 @@ Singleton { // ------------------------------------------- // Manual scaling via Settings function scale(aScreen) { - return scaleByName(aScreen.name) + try { + if (aScreen !== undefined && aScreen.name !== undefined) { + return scaleByName(aScreen.name) + } + } catch (e) { + + //Logger.warn(e) + } + return 1.0 } function scaleByName(aScreenName) { diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 322499a..a22ac03 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -3,7 +3,7 @@ import QtQuick // Example usage: // NLoader { // content: Component { -// NPanel { +// YourComponent { Loader { id: loader @@ -17,9 +17,6 @@ Loader { asynchronous: true sourceComponent: content - // onLoaded: { - // Logger.log("NLoader", "OnLoaded:", item.toString()); - // } onActiveChanged: { if (active && item && item.show) { item.show() diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8b8efe5..7398439 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -4,111 +4,180 @@ import Quickshell.Wayland import qs.Commons import qs.Services -PanelWindow { +Loader { id: root + active: false + asynchronous: true + readonly property real scaling: ScalingService.scale(screen) + property ShellScreen screen - property bool showOverlay: Settings.data.general.dimDesktop - property int topMargin: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 - property int bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 - // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) - property color overlayColor: (showOverlay && (PanelService.openedPanel === root - || isTransitioning)) ? Color.applyOpacity(Color.mShadow, - "AA") : Color.transparent - property bool isTransitioning: false - signal dismissed + property Component panelContent: null + property int rWidth: 1500 + property int rHeight: 400 + property bool rAnchorCentered: false + property bool rAnchorLeft: false + property bool rAnchorRight: false - function hide() { - // Clear the panel service when hiding - if (PanelService.openedPanel === root) { - PanelService.openedPanel = null - } - isTransitioning = false - visible = false - root.dismissed() - } + // Animation properties + readonly property real originalScale: 0.7 + readonly property real originalOpacity: 0.0 + property real scaleValue: originalScale + property real opacityValue: originalOpacity - function show() { - // Ensure only one panel is visible at a time using PanelService as ephemeral storage - try { - if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { - // Mark both panels as transitioning to prevent dimming flicker - isTransitioning = true - PanelService.openedPanel.isTransitioning = true - PanelService.openedPanel.hide() - // Small delay to ensure smooth transition - showTimer.start() - return - } - // No previous panel, show immediately - PanelService.openedPanel = root - visible = true - } catch (e) { + property alias isClosing: hideTimer.running - // ignore + signal opened + signal closed + + + + // ----------------------------------------- + function toggle(aScreen) { + if (!active || isClosing) { + open(aScreen) + } else { + close() } } - implicitWidth: screen.width - implicitHeight: screen.height - color: visible ? overlayColor : Color.transparent - visible: false - WlrLayershell.exclusionMode: ExclusionMode.Ignore - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - margins.top: topMargin - margins.bottom: bottomMargin - - MouseArea { - anchors.fill: parent - onClicked: root.hide() - } - - Behavior on color { - ColorAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic + // ----------------------------------------- + function open(aScreen) { + if (aScreen !== null) { + screen = aScreen } + + // Special case if currently closing/animating + if (isClosing) { + hideTimer.stop() // in case we were closing + scaleValue = 1.0 + opacityValue = 1.0 + } + + PanelService.registerOpen(root) + + active = true + root.opened() } + // ----------------------------------------- + function close() { + scaleValue = originalScale + opacityValue = originalOpacity + hideTimer.start() + } + + // ----------------------------------------- + function closeCompleted() { + root.closed() + active = false + } + + // ----------------------------------------- + // Timer to disable the loader after the close animation is completed Timer { - id: showTimer - interval: 50 // Small delay to ensure smooth transition + id: hideTimer + interval: Style.animationSlow repeat: false onTriggered: { - PanelService.openedPanel = root - isTransitioning = false - visible = true + closeCompleted() } } - Component.onDestruction: { - try { - if (visible && Settings.openPanel === root) - Settings.openPanel = null - } catch (e) { + // ----------------------------------------- + sourceComponent: Component { + PanelWindow { + id: panelWindow - } - } + visible: true - onVisibleChanged: { - try { - if (!visible) { - // Clear panel service when panel becomes invisible - if (PanelService.openedPanel === root) { - PanelService.openedPanel = null + // Dim desktop if required + color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Color.applyOpacity( + Color.mShadow, + "BB") : Color.transparent + + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-panel" + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal } - if (Settings.openPanel === root) { - Settings.openPanel = null - } - isTransitioning = false } - } catch (e) { + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + margins.top: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 + margins.bottom: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + Rectangle { + id: panelBackground + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + layer.enabled: true + width: rWidth + height: rHeight + + anchors { + centerIn: rAnchorCentered ? parent : null + left: !rAnchorCentered && rAnchorLeft ? parent.left : parent.center + right: !rAnchorCentered && rAnchorRight ? parent.right : parent.center + top: !rAnchorCentered && (Settings.data.bar.position === "top") ? parent.top : undefined + bottom: !rAnchorCentered && (Settings.data.bar.position === "bottom") ? parent.bottom : undefined + + // margins + topMargin: !rAnchorCentered && (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined + bottomMargin: !rAnchorCentered + && (Settings.data.bar.position === "bottom") ? Style.marginS * scaling : undefined + rightMargin: !rAnchorCentered && rAnchorRight ? Style.marginS * scaling : undefined + } + + scale: root.scaleValue + opacity: root.opacityValue + + // Animate in when component is completed + Component.onCompleted: { + root.scaleValue = 1.0 + root.opacityValue = 1.0 + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Loader { + anchors.fill: parent + sourceComponent: root.panelContent + } + } } } } From ece8e210cccfaab6a546737e05f1c494d1c2c648 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 08:52:08 -0400 Subject: [PATCH 14/35] Calendar Panel --- Modules/Bar/Clock.qml | 4 +- Modules/Calendar/Calendar.qml | 319 ++++++++++++---------------------- Widgets/NPanel.qml | 3 - 3 files changed, 114 insertions(+), 212 deletions(-) diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index 10cd1d6..1a269c9 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -24,7 +24,7 @@ Rectangle { } onEntered: { - if (!calendarPanel.isLoaded) { + if (!calendarPanel.active) { tooltip.show() } } @@ -33,7 +33,7 @@ Rectangle { } onClicked: { tooltip.hide() - calendarPanel.isLoaded = !calendarPanel.isLoaded + calendarPanel.toggle(screen) } } } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index d879d90..3c098ce 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -7,227 +7,132 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +NPanel { id: root - content: Component { - NPanel { - id: calendarPanel + rWidth: 340 * scaling + rHeight: 320 * scaling + rAnchorRight: true - // Override hide function to animate first - function hide() { - // Start hide animation - calendarRect.scaleValue = 0.8 - calendarRect.opacityValue = 0.0 + // Main Column + panelContent: ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginM * scaling + spacing: Style.marginXS * scaling - // Hide after animation completes - hideTimer.start() + // Header: Month/Year with navigation + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginM * scaling + Layout.rightMargin: Style.marginM * scaling + spacing: Style.marginS * scaling + + NIconButton { + icon: "chevron_left" + tooltipText: "Previous Month" + onClicked: { + let newDate = new Date(grid.year, grid.month - 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } } - // Connect to NPanel's dismissed signal to handle external close events + NText { + text: grid.title + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NIconButton { + icon: "chevron_right" + tooltipText: "Next Month" + onClicked: { + let newDate = new Date(grid.year, grid.month + 1, 1) + grid.year = newDate.getFullYear() + grid.month = newDate.getMonth() + } + } + } + + // Divider between header and weekdays + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginS * scaling + Layout.bottomMargin: Style.marginM * scaling + } + + // Columns label (respects locale's first day of week) + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginS * scaling // Align with grid + Layout.rightMargin: Style.marginS * scaling + spacing: 0 + + Repeater { + model: 7 + + NText { + text: { + // Use the locale's first day of week setting + let firstDay = Qt.locale().firstDayOfWeek + let dayIndex = (firstDay + index) % 7 + return Qt.locale().dayName(dayIndex, Locale.ShortFormat) + } + color: Color.mSecondary + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.preferredWidth: Style.baseWidgetSize * scaling + } + } + } + + // Grids: days + MonthGrid { + id: grid + + Layout.fillWidth: true + Layout.fillHeight: true // Take remaining space + Layout.leftMargin: Style.marginS * scaling + Layout.rightMargin: Style.marginS * scaling + spacing: 0 + month: Time.date.getMonth() + year: Time.date.getFullYear() + locale: Qt.locale() // Use system locale + + // Optionally, update when the panel becomes visible Connections { target: calendarPanel - function onDismissed() { - // Start hide animation - calendarRect.scaleValue = 0.8 - calendarRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() + function onVisibleChanged() { + if (calendarPanel.visible) { + grid.month = Time.date.getMonth() + grid.year = Time.date.getFullYear() + } } } - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && calendarRect.opacityValue > 0) { - // Start hide animation - calendarRect.scaleValue = 0.8 - calendarRect.opacityValue = 0.0 + delegate: Rectangle { + width: (Style.baseWidgetSize * scaling) + height: (Style.baseWidgetSize * scaling) + radius: Style.radiusS * scaling + color: model.today ? Color.mPrimary : Color.transparent - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - calendarPanel.visible = false - calendarPanel.dismissed() - } - } - - Rectangle { - id: calendarRect - color: Color.mSurface - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderM * scaling) - width: 340 * scaling - height: 320 * scaling // Reduced height to eliminate bottom space - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginXS * scaling - anchors.rightMargin: Style.marginXS * scaling - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 + NText { + anchors.centerIn: parent + text: model.day + color: model.today ? Color.mOnPrimary : Color.mOnSurface + opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight + font.pointSize: (Style.fontSizeM * scaling) + font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular } - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - // Main Column - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginM * scaling - spacing: Style.marginXS * scaling - - // Header: Month/Year with navigation - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: Style.marginM * scaling - Layout.rightMargin: Style.marginM * scaling - spacing: Style.marginS * scaling - - NIconButton { - icon: "chevron_left" - tooltipText: "Previous Month" - onClicked: { - let newDate = new Date(grid.year, grid.month - 1, 1) - grid.year = newDate.getFullYear() - grid.month = newDate.getMonth() - } - } - - NText { - text: grid.title - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - } - - NIconButton { - icon: "chevron_right" - tooltipText: "Next Month" - onClicked: { - let newDate = new Date(grid.year, grid.month + 1, 1) - grid.year = newDate.getFullYear() - grid.month = newDate.getMonth() - } - } - } - - // Divider between header and weekdays - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginS * scaling - Layout.bottomMargin: Style.marginM * scaling - } - - // Columns label (respects locale's first day of week) - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: Style.marginS * scaling // Align with grid - Layout.rightMargin: Style.marginS * scaling - spacing: 0 - - Repeater { - model: 7 - - NText { - text: { - // Use the locale's first day of week setting - let firstDay = Qt.locale().firstDayOfWeek - let dayIndex = (firstDay + index) % 7 - return Qt.locale().dayName(dayIndex, Locale.ShortFormat) - } - color: Color.mSecondary - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - Layout.preferredWidth: Style.baseWidgetSize * scaling - } - } - } - - // Grids: days - MonthGrid { - id: grid - - Layout.fillWidth: true - Layout.fillHeight: true // Take remaining space - Layout.leftMargin: Style.marginS * scaling - Layout.rightMargin: Style.marginS * scaling - spacing: 0 - month: Time.date.getMonth() - year: Time.date.getFullYear() - locale: Qt.locale() // Use system locale - - // Optionally, update when the panel becomes visible - Connections { - target: calendarPanel - function onVisibleChanged() { - if (calendarPanel.visible) { - grid.month = Time.date.getMonth() - grid.year = Time.date.getFullYear() - } - } - } - - delegate: Rectangle { - width: (Style.baseWidgetSize * scaling) - height: (Style.baseWidgetSize * scaling) - radius: Style.radiusS * scaling - color: model.today ? Color.mPrimary : Color.transparent - - NText { - anchors.centerIn: parent - text: model.day - color: model.today ? Color.mOnPrimary : Color.mOnSurface - opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight - font.pointSize: (Style.fontSizeM * scaling) - font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } + Behavior on color { + ColorAnimation { + duration: Style.animationFast } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7398439..3acce3e 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -31,8 +31,6 @@ Loader { signal opened signal closed - - // ----------------------------------------- function toggle(aScreen) { if (!active || isClosing) { @@ -96,7 +94,6 @@ Loader { color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Color.applyOpacity( Color.mShadow, "BB") : Color.transparent - WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" From 2a8752e81de75f705f8c7eba7611f345bc72f22e Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 09:26:40 -0400 Subject: [PATCH 15/35] WifiPanel wip --- Modules/Bar/WiFi.qml | 23 +- Modules/Bar/WiFiMenu.qml | 435 -------------------------------------- Modules/Bar/WiFiPanel.qml | 335 +++++++++++++++++++++++++++++ Widgets/NPanel.qml | 1 + 4 files changed, 339 insertions(+), 455 deletions(-) delete mode 100644 Modules/Bar/WiFiMenu.qml create mode 100644 Modules/Bar/WiFiPanel.qml diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index 203ab7f..6b9b921 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -34,27 +34,10 @@ NIconButton { } tooltipText: "WiFi Networks" onClicked: { - if (!wifiMenuLoader.active) { - wifiMenuLoader.isLoaded = true - } - if (wifiMenuLoader.item) { - if (wifiMenuLoader.item.visible) { - // Panel is visible, hide it with animation - if (wifiMenuLoader.item.hide) { - wifiMenuLoader.item.hide() - } else { - wifiMenuLoader.item.visible = false - NetworkService.onMenuClosed() - } - } else { - // Panel is hidden, show it - wifiMenuLoader.item.visible = true - NetworkService.onMenuOpened() - } - } + wifiPanel.toggle(screen) } - WiFiMenu { - id: wifiMenuLoader + WiFiPanel { + id: wifiPanel } } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml deleted file mode 100644 index f66f9cf..0000000 --- a/Modules/Bar/WiFiMenu.qml +++ /dev/null @@ -1,435 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services -import qs.Widgets - -// Loader for WiFi menu -NLoader { - id: root - - content: Component { - NPanel { - id: wifiPanel - - property string passwordPromptSsid: "" - property string passwordInput: "" - property bool showPasswordPrompt: false - - function hide() { - wifiMenuRect.scaleValue = 0.8 - wifiMenuRect.opacityValue = 0.0 - - hideTimer.start() - } - - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: wifiPanel - ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation - wifiMenuRect.scaleValue = 0.8 - wifiMenuRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Also handle visibility changes from external sources - onVisibleChanged: { - if (visible && Settings.data.network.wifiEnabled) { - NetworkService.refreshNetworks() - } else if (wifiMenuRect.opacityValue > 0) { - // Start hide animation - wifiMenuRect.scaleValue = 0.8 - wifiMenuRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - wifiPanel.visible = false - wifiPanel.dismissed() - // NetworkService.onMenuClosed() - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - // Timer to refresh networks when WiFi is enabled while menu is open - Timer { - id: wifiEnableRefreshTimer - interval: 3000 // Wait 3 seconds for WiFi to be fully ready - repeat: false - onTriggered: { - if (Settings.data.network.wifiEnabled && wifiPanel.visible) { - NetworkService.refreshNetworks() - } - } - } - - Rectangle { - id: wifiMenuRect - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - width: 340 * scaling - height: 500 * scaling - anchors { - right: parent.right - rightMargin: Style.marginXS * scaling - top: Settings.data.bar.position === "top" ? parent.top : undefined - bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined - topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined - bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined - } - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NIcon { - text: "wifi" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary - } - - NText { - text: "WiFi" - font.pointSize: Style.fontSizeL * scaling - font.bold: true - color: Color.mOnSurface - Layout.fillWidth: true - } - - NIconButton { - icon: "refresh" - tooltipText: "Refresh Networks" - sizeMultiplier: 0.8 - enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading - onClicked: { - NetworkService.refreshNetworks() - } - } - - NIconButton { - icon: "close" - tooltipText: "Close" - sizeMultiplier: 0.8 - onClicked: { - wifiPanel.hide() - } - } - } - - NDivider {} - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - // Loading indicator - ColumnLayout { - anchors.centerIn: parent - visible: Settings.data.network.wifiEnabled && NetworkService.isLoading - spacing: Style.marginM * scaling - - NBusyIndicator { - running: NetworkService.isLoading - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Scanning for networks..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - } - - // WiFi disabled message - ColumnLayout { - anchors.centerIn: parent - visible: !Settings.data.network.wifiEnabled - spacing: Style.marginM * scaling - - NIcon { - text: "wifi_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "WiFi is disabled" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Enable WiFi to see available networks" - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - } - - // Network list - ListView { - id: networkList - anchors.fill: parent - visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading - model: Object.values(NetworkService.networks) - spacing: Style.marginM * scaling - clip: true - - delegate: Item { - width: parent ? parent.width : 0 - height: modelData.ssid === passwordPromptSsid - && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling - radius: Style.radiusM * scaling - color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent) - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - NIcon { - text: NetworkService.signalIcon(modelData.signal) - font.pointSize: Style.fontSizeXXL * scaling - color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginXS * scaling - - // SSID - NText { - text: modelData.ssid || "Unknown Network" - font.pointSize: Style.fontSizeNormal * scaling - elide: Text.ElideRight - Layout.fillWidth: true - color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) - } - - // Security Protocol - NText { - text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" - font.pointSize: Style.fontSizeXS * scaling - elide: Text.ElideRight - Layout.fillWidth: true - color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) - } - - NText { - visible: NetworkService.connectStatusSsid === modelData.ssid - && NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 - text: NetworkService.connectError - color: Color.mError - font.pointSize: Style.fontSizeXS * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - } - - Item { - Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling - Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling - visible: NetworkService.connectStatusSsid === modelData.ssid - && (NetworkService.connectStatus !== "" - || NetworkService.connectingSsid === modelData.ssid) - - NBusyIndicator { - visible: NetworkService.connectingSsid === modelData.ssid - running: NetworkService.connectingSsid === modelData.ssid - color: Color.mPrimary - anchors.centerIn: parent - size: Style.baseWidgetSize * 0.7 * scaling - } - } - - NText { - visible: modelData.connected - text: "connected" - font.pointSize: Style.fontSizeXS * scaling - color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) - } - } - - MouseArea { - id: networkMouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - if (modelData.connected) { - NetworkService.disconnectNetwork(modelData.ssid) - } else if (NetworkService.isSecured(modelData.security) && !modelData.existing) { - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" // Clear previous input - Qt.callLater(function () { - passwordInputField.forceActiveFocus() - }) - } else { - NetworkService.connectNetwork(modelData.ssid, modelData.security) - } - } - } - } - - // Password prompt section - Rectangle { - id: passwordPromptSection - Layout.fillWidth: true - Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 - Layout.margins: Style.marginS * scaling - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - Item { - Layout.fillWidth: true - Layout.preferredHeight: Style.barHeight * scaling - - Rectangle { - anchors.fill: parent - radius: Style.radiusXS * scaling - color: Color.transparent - border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - TextInput { - id: passwordInputField - anchors.fill: parent - anchors.margins: Style.marginM * scaling - text: passwordInput - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: true - selectByMouse: true - activeFocusOnTab: true - inputMethodHints: Qt.ImhNone - echoMode: TextInput.Password - onTextChanged: passwordInput = text - onAccepted: { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - - MouseArea { - id: passwordInputMouseArea - anchors.fill: parent - onClicked: passwordInputField.forceActiveFocus() - } - } - } - } - - Rectangle { - Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling - Layout.preferredHeight: Style.barHeight * scaling - radius: Style.radiusM * scaling - color: Color.mPrimary - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - NText { - anchors.centerIn: parent - text: "Connect" - color: Color.mSurface - font.pointSize: Style.fontSizeXS * scaling - } - - MouseArea { - anchors.fill: parent - onClicked: { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - } - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1) - onExited: parent.color = Color.mPrimary - } - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/Modules/Bar/WiFiPanel.qml b/Modules/Bar/WiFiPanel.qml new file mode 100644 index 0000000..9e8a348 --- /dev/null +++ b/Modules/Bar/WiFiPanel.qml @@ -0,0 +1,335 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NPanel { + id: root + + rWidth: 340 * scaling + rHeight: 500 * scaling + rAnchorRight: true + + property string passwordPromptSsid: "" + property string passwordInput: "" + property bool showPasswordPrompt: false + + onOpened: { + if (Settings.data.network.wifiEnabled && wifiPanel.visible) { + NetworkService.refreshNetworks() + } + } + + panelContent: Rectangle { + color: Color.transparent + anchors.fill: parent + anchors.margins: Style.marginL * scaling + + ColumnLayout { + anchors.fill: parent + + // Header + RowLayout { + NIcon { + text: "wifi" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary + } + + NText { + text: "WiFi" + font.pointSize: Style.fontSizeL * scaling + font.bold: true + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + tooltipText: "Refresh Networks" + sizeMultiplier: 0.8 + enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading + onClicked: { + NetworkService.refreshNetworks() + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + root.close() + } + } + } + + NDivider { + Layout.fillWidth: true + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + // Loading indicator + ColumnLayout { + anchors.centerIn: parent + visible: Settings.data.network.wifiEnabled && NetworkService.isLoading + spacing: Style.marginM * scaling + + NBusyIndicator { + running: NetworkService.isLoading + color: Color.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // WiFi disabled message + ColumnLayout { + anchors.centerIn: parent + visible: !Settings.data.network.wifiEnabled + spacing: Style.marginM * scaling + + NIcon { + text: "wifi_off" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "WiFi is disabled" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable WiFi to see available networks" + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + + // Network list + ListView { + id: networkList + anchors.fill: parent + visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading + model: Object.values(NetworkService.networks) + spacing: Style.marginM * scaling + clip: true + + delegate: Item { + width: parent ? parent.width : 0 + height: modelData.ssid === passwordPromptSsid + && showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusS * scaling + color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent) + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling + + NIcon { + text: NetworkService.signalIcon(modelData.signal) + font.pointSize: Style.fontSizeXXL * scaling + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS * scaling + + // SSID + NText { + text: modelData.ssid || "Unknown Network" + font.pointSize: Style.fontSizeNormal * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + // Security Protocol + NText { + text: modelData.security && modelData.security !== "--" ? modelData.security : "Open" + font.pointSize: Style.fontSizeXS * scaling + elide: Text.ElideRight + Layout.fillWidth: true + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + + NText { + visible: NetworkService.connectStatusSsid === modelData.ssid + && NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 + text: NetworkService.connectError + color: Color.mError + font.pointSize: Style.fontSizeXS * scaling + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + Item { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling + visible: NetworkService.connectStatusSsid === modelData.ssid + && (NetworkService.connectStatus !== "" + || NetworkService.connectingSsid === modelData.ssid) + + NBusyIndicator { + visible: NetworkService.connectingSsid === modelData.ssid + running: NetworkService.connectingSsid === modelData.ssid + color: Color.mPrimary + anchors.centerIn: parent + size: Style.baseWidgetSize * 0.7 * scaling + } + } + + NText { + visible: modelData.connected + text: "connected" + font.pointSize: Style.fontSizeXS * scaling + color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface) + } + } + + MouseArea { + id: networkMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (modelData.connected) { + NetworkService.disconnectNetwork(modelData.ssid) + } else if (NetworkService.isSecured(modelData.security) && !modelData.existing) { + passwordPromptSsid = modelData.ssid + showPasswordPrompt = true + passwordInput = "" // Clear previous input + Qt.callLater(function () { + passwordInputField.forceActiveFocus() + }) + } else { + NetworkService.connectNetwork(modelData.ssid, modelData.security) + } + } + } + } + + // Password prompt section + Rectangle { + id: passwordPromptSection + Layout.fillWidth: true + Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0 + Layout.margins: Style.marginS * scaling + visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + color: Color.mSurfaceVariant + radius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + + Rectangle { + anchors.fill: parent + radius: Style.radiusXS * scaling + color: Color.transparent + border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + TextInput { + id: passwordInputField + anchors.fill: parent + anchors.margins: Style.marginM * scaling + text: passwordInput + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + verticalAlignment: TextInput.AlignVCenter + clip: true + focus: true + selectByMouse: true + activeFocusOnTab: true + inputMethodHints: Qt.ImhNone + echoMode: TextInput.Password + onTextChanged: passwordInput = text + onAccepted: { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + + MouseArea { + id: passwordInputMouseArea + anchors.fill: parent + onClicked: passwordInputField.forceActiveFocus() + } + } + } + } + + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling + Layout.preferredHeight: Style.barHeight * scaling + radius: Style.radiusM * scaling + color: Color.mPrimary + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: "Connect" + color: Color.mSurface + font.pointSize: Style.fontSizeXS * scaling + } + + MouseArea { + anchors.fill: parent + onClicked: { + NetworkService.submitPassword(passwordPromptSsid, passwordInput) + showPasswordPrompt = false + } + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1) + onExited: parent.color = Color.mPrimary + } + } + } + } + } + } + } + } + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 3acce3e..7c8f7d4 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -97,6 +97,7 @@ Loader { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "noctalia-panel" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand Behavior on color { ColorAnimation { From 30722975f33200a1b1d9a022148e8ee4949d1577 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 09:28:52 -0400 Subject: [PATCH 16/35] Removed DemoPanel --- Modules/Bar/Bar.qml | 10 -- Modules/DemoPanel/DemoPanel.qml | 307 -------------------------------- shell.qml | 5 - 3 files changed, 322 deletions(-) delete mode 100644 Modules/DemoPanel/DemoPanel.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e2e4beb..0c46802 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -119,16 +119,6 @@ Variants { anchors.verticalCenter: parent.verticalCenter } - // NIconButton { - // id: demoPanelToggle - // icon: "experiment" - // tooltipText: "Open Demo Panel" - // sizeMultiplier: 0.8 - // anchors.verticalCenter: parent.verticalCenter - // onClicked: { - // demoPanel.isLoaded = !demoPanel.isLoaded - // } - // } SidePanelToggle {} } } diff --git a/Modules/DemoPanel/DemoPanel.qml b/Modules/DemoPanel/DemoPanel.qml deleted file mode 100644 index db78737..0000000 --- a/Modules/DemoPanel/DemoPanel.qml +++ /dev/null @@ -1,307 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services -import qs.Widgets - -NLoader { - id: root - - content: Component { - NPanel { - id: demoPanel - - property real sliderValue: 1.0 - - // Override hide function to animate first - function hide() { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: demoPanel - function onDismissed() { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && bgRect.opacityValue > 0) { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - demoPanel.visible = false - demoPanel.dismissed() - } - } - - // Ensure panel shows itself once created - Component.onCompleted: { - show() - } - - Rectangle { - id: bgRect - color: Color.mSurfaceVariant - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - width: 500 * scaling - height: 900 * scaling - anchors.centerIn: parent - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginXL * scaling - - NText { - text: "DemoPanel" - color: Color.mPrimary - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - Layout.alignment: Qt.AlignHCenter - } - - ColumnLayout { - - spacing: Style.marginM * scaling - - // NSlider - ColumnLayout { - spacing: Style.marginL * scaling - NText { - text: "NSlider" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - NText { - text: `${Math.round(sliderValue * 100)}%` - Layout.alignment: Qt.AlignVCenter - } - RowLayout { - spacing: Style.marginS * scaling - NSlider { - id: scaleSlider - from: 1.0 - to: 2.0 - stepSize: 0.01 - value: sliderValue - onPressedChanged: { - sliderValue = value - } - Layout.fillWidth: true - } - NIconButton { - icon: "refresh" - tooltipText: "Reset Scaling" - fontPointSize: Style.fontSizeL * scaling - onClicked: { - sliderValue = 1.0 - } - } - } - NDivider { - Layout.fillWidth: true - } - } - - // NIconButton - ColumnLayout { - spacing: Style.marginL * scaling - NText { - text: "NIconButton" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NIconButton { - id: myIconButton - icon: "celebration" - tooltipText: "A nice tooltip" - fontPointSize: Style.fontSizeL * scaling - } - - NDivider { - Layout.fillWidth: true - } - } - - // NToggle - ColumnLayout { - spacing: Style.marginM * scaling - NText { - text: "NToggle" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NToggle { - label: "Label" - description: "Description" - onToggled: checked => { - Logger.log("DemoPanel", "NToggle:", checked) - } - } - - NDivider { - Layout.fillWidth: true - } - } - - // NComboBox - ColumnLayout { - spacing: Style.marginM * scaling - NText { - text: "NComboBox" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NComboBox { - label: "Animal" - description: "What's your favorite?" - model: ListModel { - ListElement { - key: "cat" - name: "Cat" - } - ListElement { - key: "dog" - name: "Dog" - } - ListElement { - key: "bird" - name: "Bird" - } - ListElement { - key: "fish" - name: "Fish" - } - ListElement { - key: "turtle" - name: "Turtle" - } - ListElement { - key: "elephant" - name: "Elephant" - } - ListElement { - key: "tiger" - name: "Tiger" - } - } - currentKey: "dog" - onSelected: function (key) { - Logger.log("DemoPanel", "NComboBox: selected ", key) - } - } - - NDivider { - Layout.fillWidth: true - } - } - - // NTextInput - ColumnLayout { - spacing: Style.marginM * scaling - NText { - text: "NTextInput" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NTextInput { - label: "Input label" - description: "A cool description" - text: "Type anything" - Layout.fillWidth: true - onEditingFinished: { - - } - } - NDivider { - Layout.fillWidth: true - } - } - - // NBusyIndicator - ColumnLayout { - spacing: Style.marginM * scaling - NText { - text: "NBusyIndicator" - color: Color.mSecondary - font.weight: Style.fontWeightBold - } - - NBusyIndicator {} - - NDivider { - Layout.fillWidth: true - } - } - } - } - } - } - } -} diff --git a/shell.qml b/shell.qml index de85aee..b31a5fc 100644 --- a/shell.qml +++ b/shell.qml @@ -17,7 +17,6 @@ import qs.Modules.AppLauncher import qs.Modules.Background import qs.Modules.Bar import qs.Modules.Calendar -import qs.Modules.DemoPanel import qs.Modules.Dock import qs.Modules.IPC import qs.Modules.LockScreen @@ -41,10 +40,6 @@ ShellRoot { id: appLauncherPanel } - DemoPanel { - id: demoPanel - } - SidePanel { id: sidePanel } From c7e82517f20b52a5e80f539f574a89bc4b5060fd Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 09:38:26 -0400 Subject: [PATCH 17/35] NotificationHistory panel --- Modules/Bar/NotificationHistory.qml | 17 +- .../Notification/NotificationHistoryPanel.qml | 382 +++++++----------- Modules/SidePanel/SidePanel.qml | 3 - 3 files changed, 154 insertions(+), 248 deletions(-) diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/NotificationHistory.qml index e1dbc3b..ba794b4 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/NotificationHistory.qml @@ -20,21 +20,6 @@ NIconButton { colorBorderHover: Color.transparent onClicked: { - if (!notificationHistoryPanel.active) { - notificationHistoryPanel.isLoaded = true - } - if (notificationHistoryPanel.item) { - if (notificationHistoryPanel.item.visible) { - // Panel is visible, hide it with animation - if (notificationHistoryPanel.item.hide) { - notificationHistoryPanel.item.hide() - } else { - notificationHistoryPanel.item.visible = false - } - } else { - // Panel is hidden, show it - notificationHistoryPanel.item.visible = true - } - } + notificationHistoryPanel.toggle(screen) } } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index a52950e..affcad1 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -8,267 +8,191 @@ import qs.Commons import qs.Services import qs.Widgets -// Loader for Notification History panel -NLoader { +// Notification History panel +NPanel { id: root - content: Component { - NPanel { - id: notificationPanel + rWidth: 400 * scaling + rHeight: 500 * scaling + rAnchorRight: true - // Override hide function to animate first - function hide() { - // Start hide animation - notificationRect.scaleValue = 0.8 - notificationRect.opacityValue = 0.0 + panelContent: Rectangle { + id: notificationRect + color: Color.transparent - // Hide after animation completes - hideTimer.start() - } + // color: Color.mSurface + // radius: Style.radiusL * scaling + // border.color: Color.mOutline + // border.width: Math.max(1, Style.borderS * scaling) - Connections { - target: notificationPanel - ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation - notificationRect.scaleValue = 0.8 - notificationRect.opacityValue = 0.0 + // anchors.top: parent.top + // anchors.right: parent.right + // anchors.topMargin: Style.marginXS * scaling + // anchors.rightMargin: Style.marginXS * scaling + // clip: true + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling - // Hide after animation completes - hideTimer.start() - } - } + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && notificationRect.opacityValue > 0) { - // Start hide animation - notificationRect.scaleValue = 0.8 - notificationRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - notificationPanel.visible = false - notificationPanel.dismissed() - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - id: notificationRect - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - width: 400 * scaling - height: 500 * scaling - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Style.marginXS * scaling - anchors.rightMargin: Style.marginXS * scaling - clip: true - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 + NIcon { + text: "notifications" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary } - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo + NText { + text: "Notification History" + font.pointSize: Style.fontSizeL * scaling + font.bold: true + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "delete" + tooltipText: "Clear History" + sizeMultiplier: 0.8 + onClicked: NotificationService.clearHistory() + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + root.close() } } + } - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } + NDivider { + Layout.fillWidth: true + } + + // Empty state when no notifications + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: NotificationService.historyModel.count === 0 ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling + anchors.centerIn: parent spacing: Style.marginM * scaling + NIcon { + text: "notifications_off" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No notifications" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Notifications will appear here when you receive them" + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + } + + ListView { + id: notificationList + Layout.fillWidth: true + Layout.fillHeight: true + model: NotificationService.historyModel + spacing: Style.marginM * scaling + clip: true + boundsBehavior: Flickable.StopAtBounds + visible: NotificationService.historyModel.count > 0 + + delegate: Rectangle { + width: notificationList ? (notificationList.width - 20) : 380 * scaling + height: Math.max(80, notificationContent.height + 30) + radius: Style.radiusM * scaling + color: notificationMouseArea.containsMouse ? Color.mPrimary : Color.mSurfaceVariant + RowLayout { - Layout.fillWidth: true + anchors { + fill: parent + margins: Style.marginM * scaling + } spacing: Style.marginM * scaling - NIcon { - text: "notifications" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary - } - - NText { - text: "Notification History" - font.pointSize: Style.fontSizeL * scaling - font.bold: true - color: Color.mOnSurface + // Notification content + Column { + id: notificationContent Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginXXS * scaling + + NText { + text: (summary || "No summary").substring(0, 100) + font.pointSize: Style.fontSizeM * scaling + font.weight: Font.Medium + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + wrapMode: Text.Wrap + width: parent.width - 60 + maximumLineCount: 2 + elide: Text.ElideRight + } + + NText { + text: (body || "").substring(0, 150) + font.pointSize: Style.fontSizeXS * scaling + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + wrapMode: Text.Wrap + width: parent.width - 60 + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: NotificationService.formatTimestamp(timestamp) + font.pointSize: Style.fontSizeXS * scaling + color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + } } + // Trash icon button NIconButton { icon: "delete" - tooltipText: "Clear History" - sizeMultiplier: 0.8 - onClicked: NotificationService.clearHistory() - } + tooltipText: "Delete Notification" + sizeMultiplier: 0.7 - NIconButton { - icon: "close" - tooltipText: "Close" - sizeMultiplier: 0.8 onClicked: { - notificationPanel.hide() + Logger.log("NotificationHistory", "Removing notification:", summary) + NotificationService.historyModel.remove(index) + NotificationService.saveHistory() } } } - NDivider {} - - // Empty state when no notifications - Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: NotificationService.historyModel.count === 0 - - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling - - NIcon { - text: "notifications_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "No notifications" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Notifications will appear here when you receive them" - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - } + MouseArea { + id: notificationMouseArea + anchors.fill: parent + anchors.rightMargin: Style.marginL * 3 * scaling + hoverEnabled: true + // Remove the onClicked handler since we now have a dedicated delete button } + } - ListView { - id: notificationList - Layout.fillWidth: true - Layout.fillHeight: true - model: NotificationService.historyModel - spacing: Style.marginM * scaling - clip: true - boundsBehavior: Flickable.StopAtBounds - visible: NotificationService.historyModel.count > 0 - - delegate: Rectangle { - width: notificationList ? (notificationList.width - 20) : 380 * scaling - height: Math.max(80, notificationContent.height + 30) - radius: Style.radiusM * scaling - color: notificationMouseArea.containsMouse ? Color.mPrimary : Color.mSurfaceVariant - - RowLayout { - anchors { - fill: parent - margins: Style.marginM * scaling - } - spacing: Style.marginM * scaling - - // Notification content - Column { - id: notificationContent - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginXXS * scaling - - NText { - text: (summary || "No summary").substring(0, 100) - font.pointSize: Style.fontSizeM * scaling - font.weight: Font.Medium - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface - wrapMode: Text.Wrap - width: parent.width - 60 - maximumLineCount: 2 - elide: Text.ElideRight - } - - NText { - text: (body || "").substring(0, 150) - font.pointSize: Style.fontSizeXS * scaling - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface - wrapMode: Text.Wrap - width: parent.width - 60 - maximumLineCount: 3 - elide: Text.ElideRight - } - - NText { - text: NotificationService.formatTimestamp(timestamp) - font.pointSize: Style.fontSizeXS * scaling - color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface - } - } - - // Trash icon button - NIconButton { - icon: "delete" - tooltipText: "Delete Notification" - sizeMultiplier: 0.7 - - onClicked: { - Logger.log("NotificationHistory", "Removing notification:", summary) - NotificationService.historyModel.remove(index) - NotificationService.saveHistory() - } - } - } - - MouseArea { - id: notificationMouseArea - anchors.fill: parent - anchors.rightMargin: Style.marginL * 3 * scaling - hoverEnabled: true - // Remove the onClicked handler since we now have a dedicated delete button - } - } - - ScrollBar.vertical: ScrollBar { - active: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - } + ScrollBar.vertical: ScrollBar { + active: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 58b3065..6380a72 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -14,9 +14,6 @@ NPanel { rHeight: 700 * scaling rAnchorRight: true - // rectX: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, - // Math.round(anchorX - width / 2))) - // rectY: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined panelContent: Item { id: content From 41bc827ca9484737c79d6a55ef4c930f0ff6c22f Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 09:40:14 -0400 Subject: [PATCH 18/35] NotificationHistory cleanup --- Modules/Notification/NotificationHistoryPanel.qml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index affcad1..d7b2f21 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -20,16 +20,6 @@ NPanel { id: notificationRect color: Color.transparent - // color: Color.mSurface - // radius: Style.radiusL * scaling - // border.color: Color.mOutline - // border.width: Math.max(1, Style.borderS * scaling) - - // anchors.top: parent.top - // anchors.right: parent.right - // anchors.topMargin: Style.marginXS * scaling - // anchors.rightMargin: Style.marginXS * scaling - // clip: true ColumnLayout { anchors.fill: parent anchors.margins: Style.marginL * scaling @@ -184,7 +174,6 @@ NPanel { anchors.fill: parent anchors.rightMargin: Style.marginL * 3 * scaling hoverEnabled: true - // Remove the onClicked handler since we now have a dedicated delete button } } From c3206881a3c7e4d7ec39b248a92331e5be7dd9e3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 15:40:34 +0200 Subject: [PATCH 19/35] Add NSpinBox, use in BrightnessTab, AudioTab --- Commons/Settings.qml | 1 + Modules/SettingsPanel/Tabs/AudioTab.qml | 21 ++ Modules/SettingsPanel/Tabs/BrightnessTab.qml | 35 +--- README.md | 2 +- Services/AudioService.qml | 3 +- Widgets/NImageCached.qml | 2 +- Widgets/NSpinBox.qml | 203 +++++++++++++++++++ 7 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 Widgets/NSpinBox.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 3b9466a..a7693ff 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -217,6 +217,7 @@ Singleton { audio: JsonObject { property string visualizerType: "linear" + property int volumeStep: 5 } // ui diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 98fba12..ffc225e 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -120,6 +120,27 @@ ColumnLayout { } } } + + // Volume Step Size + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + NSpinBox { + Layout.fillWidth: true + label: "Volume Step Size" + description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)." + minimum: 1 + maximum: 25 + value: Settings.data.audio.volumeStep + stepSize: 1 + suffix: "%" + onValueChanged: { + Settings.data.audio.volumeStep = value + } + } + } } NDivider { diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index f5c8270..3b61e7b 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -49,34 +49,17 @@ Item { spacing: Style.marginS * scaling Layout.fillWidth: true - NLabel { + NSpinBox { + Layout.fillWidth: true label: "Brightness Step Size" description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)." - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NSlider { - Layout.fillWidth: true - from: 1 - to: 50 - value: Settings.data.brightness.brightnessStep - stepSize: 1 - onPressedChanged: { - if (!pressed) { - Settings.data.brightness.brightnessStep = value - } - } - } - - NText { - text: Settings.data.brightness.brightnessStep + "%" - Layout.alignment: Qt.AlignVCenter - color: Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold + minimum: 1 + maximum: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + suffix: "%" + onValueChanged: { + Settings.data.brightness.brightnessStep = value } } } diff --git a/README.md b/README.md index fbf418c..6bde91f 100644 --- a/README.md +++ b/README.md @@ -250,4 +250,4 @@ Thank you to everyone who supports me and this project 💜! ## License -This project is licensed under the terms of the [MIT License](./LICENSE). +This project is licensed under the terms of the [MIT License](./LICENSE). \ No newline at end of file diff --git a/Services/AudioService.qml b/Services/AudioService.qml index f11813d..0dd6fc9 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire +import qs.Commons Singleton { id: root @@ -34,7 +35,7 @@ Singleton { readonly property alias muted: root._muted property bool _muted: !!sink?.audio?.muted - readonly property real stepVolume: 0.05 + readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 PwObjectTracker { objects: [...root.sinks, ...root.sources] diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml index 61de0a8..88e448e 100644 --- a/Widgets/NImageCached.qml +++ b/Widgets/NImageCached.qml @@ -47,4 +47,4 @@ Image { }) } } -} +} \ No newline at end of file diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml new file mode 100644 index 0000000..493bc15 --- /dev/null +++ b/Widgets/NSpinBox.qml @@ -0,0 +1,203 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +RowLayout { + id: root + + // Public properties + property alias value: spinBox.value + property alias from: spinBox.from + property alias to: spinBox.to + property alias stepSize: spinBox.stepSize + property string suffix: "" + property string prefix: "" + property string label: "" + property string description: "" + property bool enabled: true + property bool hovering: false + property int baseSize: Style.baseWidgetSize + + // Convenience properties for common naming + property alias minimum: spinBox.from + property alias maximum: spinBox.to + + signal entered + signal exited + + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NText { + text: label + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + visible: label !== "" + } + + NText { + text: description + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + visible: description !== "" + } + } + + // Simple value display with subtle controls + Rectangle { + id: spinBoxContainer + + implicitWidth: 100 * scaling // Wider for better proportions + implicitHeight: (root.baseSize - 4) * scaling // Slightly shorter than toggle + radius: height * 0.5 // Fully rounded like toggle + color: Color.mSurfaceVariant + border.color: root.hovering ? Color.mPrimary : Color.mOutline + border.width: 1 + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + + // Mouse area for scroll wheel and hover + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + onEntered: { + root.hovering = true + root.entered() + } + onExited: { + root.hovering = false + root.exited() + } + onWheel: function(wheel) { + if (wheel.angleDelta.y > 0 && spinBox.value < spinBox.to) { + spinBox.increase() + } else if (wheel.angleDelta.y < 0 && spinBox.value > spinBox.from) { + spinBox.decrease() + } + } + } + + // Decrease button (left) + Rectangle { + id: decreaseButton + width: parent.height * 0.8 // Make it circular + height: parent.height * 0.8 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: parent.height * 0.1 + radius: width * 0.5 // Perfect circle + color: decreaseArea.containsMouse ? Color.mPrimary : "transparent" + opacity: root.enabled && spinBox.value > spinBox.from ? 1.0 : 0.3 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + + NIcon { + anchors.centerIn: parent + text: "remove" + font.pointSize: Style.fontSizeS * scaling + color: decreaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + } + + MouseArea { + id: decreaseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled && spinBox.value > spinBox.from + onClicked: spinBox.decrease() + } + } + + // Increase button (right) + Rectangle { + id: increaseButton + width: parent.height * 0.8 // Make it circular + height: parent.height * 0.8 + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: parent.height * 0.1 + radius: width * 0.5 // Perfect circle + color: increaseArea.containsMouse ? Color.mPrimary : "transparent" + opacity: root.enabled && spinBox.value < spinBox.to ? 1.0 : 0.3 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + + NIcon { + anchors.centerIn: parent + text: "add" + font.pointSize: Style.fontSizeS * scaling + color: increaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + } + + MouseArea { + id: increaseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled && spinBox.value < spinBox.to + onClicked: spinBox.increase() + } + } + + // Center value display + SpinBox { + id: spinBox + anchors.left: decreaseButton.right + anchors.right: increaseButton.left + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 4 * scaling + height: parent.height + + background: Item {} + up.indicator: Item {} + down.indicator: Item {} + + font.pointSize: Style.fontSizeM * scaling + font.family: Settings.data.ui.fontDefault + + from: 0 + to: 100 + stepSize: 1 + editable: false // Only use buttons/scroll + enabled: root.enabled + + contentItem: Item { + anchors.fill: parent + + NText { + anchors.centerIn: parent + text: root.prefix + spinBox.value + root.suffix + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + } + } + } + } +} From 1658c53393834b080ef1b285a6eba1ac6961ef5d Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 09:45:12 -0400 Subject: [PATCH 20/35] BluetoothPanel --- Modules/Bar/Bluetooth.qml | 21 +- Modules/Bar/BluetoothMenu.qml | 496 --------------------------------- Modules/Bar/BluetoothPanel.qml | 398 ++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 514 deletions(-) delete mode 100644 Modules/Bar/BluetoothMenu.qml create mode 100644 Modules/Bar/BluetoothPanel.qml diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Bluetooth.qml index 7257bc8..adc76e0 100644 --- a/Modules/Bar/Bluetooth.qml +++ b/Modules/Bar/Bluetooth.qml @@ -31,25 +31,10 @@ NIconButton { } tooltipText: "Bluetooth Devices" onClicked: { - if (!bluetoothMenuLoader.active) { - bluetoothMenuLoader.isLoaded = true - } - if (bluetoothMenuLoader.item) { - if (bluetoothMenuLoader.item.visible) { - // Panel is visible, hide it with animation - if (bluetoothMenuLoader.item.hide) { - bluetoothMenuLoader.item.hide() - } else { - bluetoothMenuLoader.item.visible = false - } - } else { - // Panel is hidden, show it - bluetoothMenuLoader.item.visible = true - } - } + bluetoothPanel.toggle(screen) } - BluetoothMenu { - id: bluetoothMenuLoader + BluetoothPanel { + id: bluetoothPanel } } diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml deleted file mode 100644 index e0eff50..0000000 --- a/Modules/Bar/BluetoothMenu.qml +++ /dev/null @@ -1,496 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Bluetooth -import Quickshell.Wayland -import qs.Commons -import qs.Services -import qs.Widgets - -// Loader for Bluetooth menu -NLoader { - id: root - - content: Component { - NPanel { - id: bluetoothPanel - - function hide() { - bluetoothMenuRect.scaleValue = 0.8 - bluetoothMenuRect.opacityValue = 0.0 - hideTimer.start() - } - - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: bluetoothPanel - ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation - bluetoothMenuRect.scaleValue = 0.8 - bluetoothMenuRect.opacityValue = 0.0 - // Hide after animation completes - hideTimer.start() - } - } - - // Also handle visibility changes from external sources - onVisibleChanged: { - if (visible && Settings.data.network.bluetoothEnabled) { - // Always refresh devices when menu opens to get fresh device objects - BluetoothService.adapter.discovering = true - } else if (bluetoothMenuRect.opacityValue > 0) { - // Start hide animation - bluetoothMenuRect.scaleValue = 0.8 - bluetoothMenuRect.opacityValue = 0.0 - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - bluetoothPanel.visible = false - bluetoothPanel.dismissed() - } - } - - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Rectangle { - id: bluetoothMenuRect - - property var deviceData: null - - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - width: 380 * scaling - height: 500 * scaling - anchors { - right: parent.right - rightMargin: Style.marginXS * scaling - top: Settings.data.bar.position === "top" ? parent.top : undefined - bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined - topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined - bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined - } - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Prevent closing the window if clicking inside it - MouseArea { - anchors.fill: parent - } - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling - - // HEADER - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NIcon { - text: "bluetooth" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary - } - - NText { - text: "Bluetooth" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - Layout.fillWidth: true - } - - NIconButton { - icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh" - tooltipText: "Refresh Devices" - sizeMultiplier: 0.8 - onClicked: { - if (BluetoothService.adapter) { - BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering - } - } - } - - NIconButton { - icon: "close" - tooltipText: "Close" - sizeMultiplier: 0.8 - onClicked: { - bluetoothPanel.hide() - } - } - } - - NDivider {} - - ScrollView { - id: scrollView - - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - // Available devices - Column { - id: column - - width: parent.width - spacing: Style.marginM * scaling - visible: BluetoothService.adapter && BluetoothService.adapter.enabled - - RowLayout { - width: parent.width - spacing: Style.marginM * scaling - - NText { - text: "Available Devices" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - font.weight: Style.fontWeightMedium - } - } - - Repeater { - model: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) - return [] - - var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing - && !dev.blocked && (dev.signalStrength === undefined - || dev.signalStrength > 0) - }) - return BluetoothService.sortDevices(filtered) - } - - Rectangle { - property bool canConnect: BluetoothService.canConnect(modelData) - property bool isBusy: BluetoothService.isDeviceBusy(modelData) - - width: parent.width - height: 70 - radius: Style.radiusM * scaling - color: { - if (availableDeviceArea.containsMouse && !isBusy) - return Color.mTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mPrimary - - if (modelData.blocked) - return Color.mError - - return Color.mSurfaceVariant - } - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - Row { - anchors.left: parent.left - anchors.leftMargin: Style.marginM * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - - // One device BT icon - NIcon { - text: BluetoothService.getDeviceIcon(modelData) - font.pointSize: Style.fontSizeXXL * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: Style.marginXXS * scaling - anchors.verticalCenter: parent.verticalCenter - - // One device name - NText { - text: modelData.name || modelData.deviceName - font.pointSize: Style.fonttSizeMedium * scaling - elide: Text.ElideRight - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - font.weight: Style.fontWeightMedium - } - - Row { - spacing: Style.marginXS * scaling - - Row { - spacing: Style.marginS * spacing - - // One device signal strength - "Unknown" when not connected - NText { - text: { - if (modelData.pairing) - return "Pairing..." - - if (modelData.blocked) - return "Blocked" - - return BluetoothService.getSignalStrength(modelData) - } - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - } - - NIcon { - text: BluetoothService.getSignalIcon(modelData) - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - - NText { - text: (modelData.signalStrength !== undefined - && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - } - } - } - } - - Rectangle { - width: 80 * scaling - height: 28 * scaling - radius: Style.radiusM * scaling - anchors.right: parent.right - anchors.rightMargin: Style.marginM * scaling - anchors.verticalCenter: parent.verticalCenter - visible: modelData.state !== BluetoothDeviceState.Connecting - color: Color.transparent - - border.color: { - if (availableDeviceArea.containsMouse) { - return Color.mOnTertiary - } else { - return Color.mPrimary - } - } - border.width: Math.max(1, Style.borderS * scaling) - opacity: canConnect || isBusy ? 1 : 0.5 - - // On device connect button - NText { - anchors.centerIn: parent - text: { - if (modelData.pairing) - return "Pairing..." - - if (modelData.blocked) - return "Blocked" - - return "Connect" - } - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - color: { - if (availableDeviceArea.containsMouse) { - return Color.mOnTertiary - } else { - return Color.mPrimary - } - } - } - } - - MouseArea { - id: availableDeviceArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: canConnect - && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) - enabled: canConnect && !isBusy - onClicked: { - if (modelData) - BluetoothService.connectDeviceWithTrust(modelData) - } - } - } - } - - // Fallback if nothing available - Column { - width: parent.width - spacing: Style.marginM * scaling - visible: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) - return false - - var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing - && !dev.blocked - && (dev.signalStrength === undefined - || dev.signalStrength > 0) - }).length - return availableCount === 0 - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - spacing: Style.marginM * scaling - - NIcon { - text: "sync" - font.pointSize: Style.fontSizeXLL * 1.5 * scaling - color: Color.mPrimary - anchors.verticalCenter: parent.verticalCenter - - RotationAnimation on rotation { - running: true - loops: Animation.Infinite - from: 0 - to: 360 - duration: 2000 - } - } - - NText { - text: "Scanning for devices..." - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - } - } - - NText { - text: "Make sure your device is in pairing mode" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - anchors.horizontalCenter: parent.horizontalCenter - } - } - - NText { - text: "No devices found. Put your device in pairing mode and click Start Scanning." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - visible: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return true - - var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing - && !dev.blocked - && (dev.signalStrength === undefined - || dev.signalStrength > 0) - }).length - return availableCount === 0 && !BluetoothService.adapter.discovering - } - wrapMode: Text.WordWrap - width: parent.width - horizontalAlignment: Text.AlignHCenter - } - } - } - // This item takes up all the remaining vertical space. - Item { - Layout.fillHeight: true - } - } - } - } - } -} diff --git a/Modules/Bar/BluetoothPanel.qml b/Modules/Bar/BluetoothPanel.qml new file mode 100644 index 0000000..b8b7222 --- /dev/null +++ b/Modules/Bar/BluetoothPanel.qml @@ -0,0 +1,398 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NPanel { + id: root + + rWidth: 380 * scaling + rHeight: 500 * scaling + rAnchorRight: true + + panelContent: Rectangle { + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // HEADER + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NIcon { + text: "bluetooth" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary + } + + NText { + text: "Bluetooth" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh" + tooltipText: "Refresh Devices" + sizeMultiplier: 0.8 + onClicked: { + if (BluetoothService.adapter) { + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering + } + } + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: { + root.close() + } + } + } + + NDivider { + Layout.fillWidth: true + } + + ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + // Available devices + Column { + id: column + + width: parent.width + spacing: Style.marginM * scaling + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + + RowLayout { + width: parent.width + spacing: Style.marginM * scaling + + NText { + text: "Available Devices" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + } + } + + Repeater { + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return [] + + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing && !dev.blocked + && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }) + return BluetoothService.sortDevices(filtered) + } + + Rectangle { + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + width: parent.width + height: 70 + radius: Style.radiusM * scaling + color: { + if (availableDeviceArea.containsMouse && !isBusy) + return Color.mTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mPrimary + + if (modelData.blocked) + return Color.mError + + return Color.mSurfaceVariant + } + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + Row { + anchors.left: parent.left + anchors.leftMargin: Style.marginM * scaling + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + + // One device BT icon + NIcon { + text: BluetoothService.getDeviceIcon(modelData) + font.pointSize: Style.fontSizeXXL * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Style.marginXXS * scaling + anchors.verticalCenter: parent.verticalCenter + + // One device name + NText { + text: modelData.name || modelData.deviceName + font.pointSize: Style.fonttSizeMedium * scaling + elide: Text.ElideRight + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + font.weight: Style.fontWeightMedium + } + + Row { + spacing: Style.marginXS * scaling + + Row { + spacing: Style.marginS * spacing + + // One device signal strength - "Unknown" when not connected + NText { + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return BluetoothService.getSignalStrength(modelData) + } + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + } + + NIcon { + text: BluetoothService.getSignalIcon(modelData) + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } + + NText { + text: (modelData.signalStrength !== undefined + && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 + && !modelData.pairing && !modelData.blocked + } + } + } + } + } + + Rectangle { + width: 80 * scaling + height: 28 * scaling + radius: Style.radiusM * scaling + anchors.right: parent.right + anchors.rightMargin: Style.marginM * scaling + anchors.verticalCenter: parent.verticalCenter + visible: modelData.state !== BluetoothDeviceState.Connecting + color: Color.transparent + + border.color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + border.width: Math.max(1, Style.borderS * scaling) + opacity: canConnect || isBusy ? 1 : 0.5 + + // On device connect button + NText { + anchors.centerIn: parent + text: { + if (modelData.pairing) + return "Pairing..." + + if (modelData.blocked) + return "Blocked" + + return "Connect" + } + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + } + } + + MouseArea { + id: availableDeviceArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + enabled: canConnect && !isBusy + onClicked: { + if (modelData) + BluetoothService.connectDeviceWithTrust(modelData) + } + } + } + } + + // Fallback if nothing available + Column { + width: parent.width + spacing: Style.marginM * scaling + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false + + var availableCount = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked + && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }).length + return availableCount === 0 + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.marginM * scaling + + NIcon { + text: "sync" + font.pointSize: Style.fontSizeXLL * 1.5 * scaling + color: Color.mPrimary + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + + NText { + text: "Scanning for devices..." + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + font.weight: Style.fontWeightMedium + anchors.verticalCenter: parent.verticalCenter + } + } + + NText { + text: "Make sure your device is in pairing mode" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + anchors.horizontalCenter: parent.horizontalCenter + } + } + + NText { + text: "No devices found. Put your device in pairing mode and click Start Scanning." + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + visible: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return true + + var availableCount = Bluetooth.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing + && !dev.blocked + && (dev.signalStrength === undefined + || dev.signalStrength > 0) + }).length + return availableCount === 0 && !BluetoothService.adapter.discovering + } + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + } + } + // This item takes up all the remaining vertical space. + Item { + Layout.fillHeight: true + } + } + } +} From 66e2c984769f3382751a14abf925872800d61ed9 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 15:53:31 +0200 Subject: [PATCH 21/35] More NSpinBox changes --- Widgets/NSpinBox.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index 493bc15..fe9a6ff 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -52,7 +52,7 @@ RowLayout { } } - // Simple value display with subtle controls + // Value Rectangle { id: spinBoxContainer From 50e1de1dc162a607f114a728571df439b2087fcc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 10:36:45 -0400 Subject: [PATCH 22/35] Renamed properties so its easier to understand --- Modules/Bar/BluetoothPanel.qml | 6 ++-- Modules/Bar/WiFiPanel.qml | 6 ++-- Modules/Calendar/Calendar.qml | 6 ++-- .../Notification/NotificationHistoryPanel.qml | 6 ++-- Modules/SettingsPanel/SettingsPanel.qml | 6 ++-- Modules/SidePanel/SidePanel.qml | 6 ++-- Widgets/NPanel.qml | 31 ++++++++++--------- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Modules/Bar/BluetoothPanel.qml b/Modules/Bar/BluetoothPanel.qml index b8b7222..eeb4c7f 100644 --- a/Modules/Bar/BluetoothPanel.qml +++ b/Modules/Bar/BluetoothPanel.qml @@ -11,9 +11,9 @@ import qs.Widgets NPanel { id: root - rWidth: 380 * scaling - rHeight: 500 * scaling - rAnchorRight: true + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: true panelContent: Rectangle { color: Color.transparent diff --git a/Modules/Bar/WiFiPanel.qml b/Modules/Bar/WiFiPanel.qml index 9e8a348..7d28bd1 100644 --- a/Modules/Bar/WiFiPanel.qml +++ b/Modules/Bar/WiFiPanel.qml @@ -10,9 +10,9 @@ import qs.Widgets NPanel { id: root - rWidth: 340 * scaling - rHeight: 500 * scaling - rAnchorRight: true + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: true property string passwordPromptSsid: "" property string passwordInput: "" diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 3c098ce..762c8ff 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -10,9 +10,9 @@ import qs.Widgets NPanel { id: root - rWidth: 340 * scaling - rHeight: 320 * scaling - rAnchorRight: true + panelWidth: 340 * scaling + panelHeight: 320 * scaling + panelAnchorRight: true // Main Column panelContent: ColumnLayout { diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index d7b2f21..6c7e465 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -12,9 +12,9 @@ import qs.Widgets NPanel { id: root - rWidth: 400 * scaling - rHeight: 500 * scaling - rAnchorRight: true + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: true panelContent: Rectangle { id: notificationRect diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index b4b5ff4..43fb19c 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -11,9 +11,9 @@ import qs.Widgets NPanel { id: root - rWidth: Math.max(screen?.width * 0.5, 1280) * scaling - rHeight: Math.max(screen?.height * 0.5, 720) * scaling - rAnchorCentered: true + panelWidth: Math.max(screen?.width * 0.5, 1280) * scaling + panelHeight: Math.max(screen?.height * 0.5, 720) * scaling + panelAnchorCentered: true // Tabs enumeration, order is NOT relevant enum Tab { diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 6380a72..bfbff83 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -10,9 +10,9 @@ import qs.Widgets NPanel { id: panel - rWidth: 460 * scaling - rHeight: 700 * scaling - rAnchorRight: true + panelWidth: 460 * scaling + panelHeight: 700 * scaling + panelAnchorRight: true panelContent: Item { id: content diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7c8f7d4..9dd2aa4 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -14,11 +14,11 @@ Loader { property ShellScreen screen property Component panelContent: null - property int rWidth: 1500 - property int rHeight: 400 - property bool rAnchorCentered: false - property bool rAnchorLeft: false - property bool rAnchorRight: false + property int panelWidth: 1500 + property int panelHeight: 400 + property bool panelAnchorCentered: false + property bool panelAnchorLeft: false + property bool panelAnchorRight: false // Animation properties readonly property real originalScale: 0.7 @@ -125,21 +125,22 @@ Loader { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) layer.enabled: true - width: rWidth - height: rHeight + width: panelWidth + height: panelHeight anchors { - centerIn: rAnchorCentered ? parent : null - left: !rAnchorCentered && rAnchorLeft ? parent.left : parent.center - right: !rAnchorCentered && rAnchorRight ? parent.right : parent.center - top: !rAnchorCentered && (Settings.data.bar.position === "top") ? parent.top : undefined - bottom: !rAnchorCentered && (Settings.data.bar.position === "bottom") ? parent.bottom : undefined + centerIn: panelAnchorCentered ? parent : null + left: !panelAnchorCentered && panelAnchorLeft ? parent.left : parent.center + right: !panelAnchorCentered && panelAnchorRight ? parent.right : parent.center + top: !panelAnchorCentered && (Settings.data.bar.position === "top") ? parent.top : undefined + bottom: !panelAnchorCentered && (Settings.data.bar.position === "bottom") ? parent.bottom : undefined // margins - topMargin: !rAnchorCentered && (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined - bottomMargin: !rAnchorCentered + topMargin: !panelAnchorCentered + && (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined + bottomMargin: !panelAnchorCentered && (Settings.data.bar.position === "bottom") ? Style.marginS * scaling : undefined - rightMargin: !rAnchorCentered && rAnchorRight ? Style.marginS * scaling : undefined + rightMargin: !panelAnchorCentered && panelAnchorRight ? Style.marginS * scaling : undefined } scale: root.scaleValue From 350fc2e0030d235aeae7ef2c63d1833b5046a374 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 10:37:00 -0400 Subject: [PATCH 23/35] TrayMenu adapted without NPanel. --- Modules/Bar/Tray.qml | 117 ++++++++++--------------------------------- 1 file changed, 27 insertions(+), 90 deletions(-) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 85f79c3..ee12a69 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -76,37 +76,34 @@ Rectangle { if (mouse.button === Qt.LeftButton) { // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() - } + trayPanel.close() if (!modelData.onlyMenu) { modelData.activate() } } else if (mouse.button === Qt.MiddleButton) { // Close any open menu first - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() - } + trayPanel.close() modelData.secondaryActivate && modelData.secondaryActivate() } else if (mouse.button === Qt.RightButton) { trayTooltip.hide() - // If menu is already visible, close it - if (trayMenu && trayMenu.visible) { - trayMenu.hideMenu() + + // Close the menu if it was visible + if (trayPanel && trayPanel.visible) { + trayPanel.close() return } if (modelData.hasMenu && modelData.menu && trayMenu) { + trayPanel.open() + // Anchor the menu to the tray icon item (parent) and position it below the icon const menuX = (width / 2) - (trayMenu.width / 2) const menuY = (Style.barHeight * scaling) trayMenu.menu = modelData.menu trayMenu.showAt(parent, menuX, menuY) - trayPanel.show() } else { - Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") } } @@ -125,93 +122,33 @@ Rectangle { } } - // Attached TrayMenu drop down - // Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu - NPanel { + PanelWindow { id: trayPanel + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + visible: false + color: Color.transparent + screen: screen - // Override hide function to animate first - function hide() { - // Start hide animation - trayMenuRect.scaleValue = 0.8 - trayMenuRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() + function open() { + visible = true } - Connections { - target: trayPanel - ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation - trayMenuRect.scaleValue = 0.8 - trayMenuRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } + function close() { + visible = false + trayMenu.hideMenu() } - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && trayMenuRect.opacityValue > 0) { - // Start hide animation - trayMenuRect.scaleValue = 0.8 - trayMenuRect.opacityValue = 0.0 - - // Hide after animation completes - hideTimer.start() - } - } - - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - trayPanel.visible = false - trayMenu.hideMenu() - } - } - - Rectangle { - id: trayMenuRect - color: Color.transparent + // Clicking outside of the rectangle to close + MouseArea { anchors.fill: parent + onClicked: trayPanel.close() + } - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - TrayMenu { - id: trayMenu - } + TrayMenu { + id: trayMenu } } } From c0a068ddc0d75e0cd7bbc42a619d328283439e32 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 11:01:06 -0400 Subject: [PATCH 24/35] AppLauncher --- Modules/AppLauncher/AppLauncher.qml | 26 +++++++------------------- Modules/Bar/Tray.qml | 4 ++++ Modules/IPC/IPCManager.qml | 7 ++++--- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 502f95d..89d8ca6 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -12,15 +12,12 @@ import qs.Widgets import "../../Helpers/FuzzySort.js" as Fuzzysort -NLoader { - id: appLauncher - isLoaded: false - - content: Component { NPanel { - id: appLauncherPanel + id: root + panelWidth: Math.min(700 * scaling, screen?.width * 0.75) + panelHeight: Math.min(550 * scaling, screen?.height * 0.8) + panelAnchorCentered: true - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand // Import modular components Calculator { @@ -158,7 +155,7 @@ NLoader { } else { modelData.execute() } - appLauncherPanel.hide() + root.close() } } @@ -174,14 +171,7 @@ NLoader { } // Main content container - Rectangle { - anchors.centerIn: parent - width: Math.min(700 * scaling, parent.width * 0.75) - height: Math.min(550 * scaling, parent.height * 0.8) - radius: Style.radiusL * scaling - color: Color.mSurface - border.color: Color.mOutline - border.width: Style.borderS * scaling + panelContent: Rectangle { // Subtle gradient background gradient: Gradient { @@ -262,7 +252,7 @@ NLoader { Keys.onUpPressed: selectPrev() Keys.onEnterPressed: activateSelected() Keys.onReturnPressed: activateSelected() - Keys.onEscapePressed: appLauncherPanel.hide() + Keys.onEscapePressed: root.close() } } @@ -453,5 +443,3 @@ NLoader { } } } - } -} diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index ee12a69..c6ba8c9 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -134,6 +134,10 @@ Rectangle { function open() { visible = true + + // Register into the panel service + // so this will autoclose if we open another panel + PanelService.registerOpen(trayPanel) } function close() { diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 28107c2..83e350a 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Quickshell.Io import qs.Services @@ -9,7 +10,7 @@ Item { target: "settings" function toggle() { - settingsPanel.isLoaded = !settingsPanel.isLoaded + settingsPanel.toggle(Quickshell.screens[0]) } } @@ -17,7 +18,7 @@ Item { target: "notifications" function toggleHistory() { - notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded + notificationHistoryPanel.toggle(Quickshell.screens[0]) } function toggleDoNotDisturb() {// TODO @@ -36,7 +37,7 @@ Item { target: "appLauncher" function toggle() { - appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded + appLauncherPanel.toggle(Quickshell.screens[0]) } } From 0fca050ce3a118dba33116aec156dc6c84d29d27 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 11:40:16 -0400 Subject: [PATCH 25/35] formatting --- Commons/Settings.qml | 4 +- Modules/AppLauncher/Calculator.qml | 44 +++++++++------------- Modules/AppLauncher/ClipboardHistory.qml | 11 +++--- Modules/LockScreen/LockScreen.qml | 4 +- Services/ClipboardService.qml | 42 +++++++++++---------- Services/IdleInhibitorService.qml | 41 ++++++++++---------- Widgets/NImageCached.qml | 2 +- Widgets/NSpinBox.qml | 48 ++++++++++++------------ 8 files changed, 93 insertions(+), 103 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index bf2f637..b4b240f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -224,10 +224,10 @@ Singleton { property string fontDefault: "Roboto" // Default font for all text property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays - + // Legacy compatibility property string fontFamily: fontDefault // Keep for backward compatibility - + // Idle inhibitor state property bool idleInhibitorEnabled: false } diff --git a/Modules/AppLauncher/Calculator.qml b/Modules/AppLauncher/Calculator.qml index 8675f0d..8dae5bd 100644 --- a/Modules/AppLauncher/Calculator.qml +++ b/Modules/AppLauncher/Calculator.qml @@ -34,20 +34,15 @@ QtObject { } else { // Fallback to basic evaluation console.log("AdvancedMath not available, using basic eval") - + // Basic preprocessing for common functions - var processed = expression.trim() - .replace(/\bpi\b/gi, Math.PI) - .replace(/\be\b/gi, Math.E) - .replace(/\bsqrt\s*\(/g, 'Math.sqrt(') - .replace(/\bsin\s*\(/g, 'Math.sin(') - .replace(/\bcos\s*\(/g, 'Math.cos(') - .replace(/\btan\s*\(/g, 'Math.tan(') - .replace(/\blog\s*\(/g, 'Math.log10(') - .replace(/\bln\s*\(/g, 'Math.log(') - .replace(/\bexp\s*\(/g, 'Math.exp(') - .replace(/\bpow\s*\(/g, 'Math.pow(') - .replace(/\babs\s*\(/g, 'Math.abs(') + var processed = expression.trim( + ).replace(/\bpi\b/gi, + Math.PI).replace(/\be\b/gi, + Math.E).replace(/\bsqrt\s*\(/g, + 'Math.sqrt(').replace(/\bsin\s*\(/g, + 'Math.sin(').replace(/\bcos\s*\(/g, + 'Math.cos(').replace(/\btan\s*\(/g, 'Math.tan(').replace(/\blog\s*\(/g, 'Math.log10(').replace(/\bln\s*\(/g, 'Math.log(').replace(/\bexp\s*\(/g, 'Math.exp(').replace(/\bpow\s*\(/g, 'Math.pow(').replace(/\babs\s*\(/g, 'Math.abs(') // Sanitize and evaluate if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) { @@ -90,15 +85,13 @@ QtObject { "name": "Invalid expression", "content": evaluation.error, "icon": "error", - "execute": function () { - // Do nothing for invalid expressions + "execute": function () {// Do nothing for invalid expressions } } } - const displayName = searchContext === "calc" - ? `${expression} = ${evaluation.displayResult}` - : `${expression} = ${evaluation.displayResult}` + const displayName = searchContext + === "calc" ? `${expression} = ${evaluation.displayResult}` : `${expression} = ${evaluation.displayResult}` return { "isCalculator": true, @@ -110,12 +103,10 @@ QtObject { "execute": function () { Quickshell.clipboardText = evaluation.displayResult // Also copy using shell command for better compatibility - Quickshell.execDetached(["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`]) - Quickshell.execDetached([ - "notify-send", - "Calculator", - `${expression} = ${evaluation.displayResult} (copied to clipboard)` - ]) + Quickshell.execDetached( + ["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`]) + Quickshell.execDetached( + ["notify-send", "Calculator", `${expression} = ${evaluation.displayResult} (copied to clipboard)`]) } } } @@ -127,8 +118,7 @@ QtObject { "name": "Calculator", "content": "Try: sqrt(16), sin(1), cos(0), pi*2, exp(1), pow(2,8), abs(-5)", "icon": "calculate", - "execute": function () { - // Do nothing for placeholder + "execute": function () {// Do nothing for placeholder } } } @@ -149,7 +139,7 @@ QtObject { // Handle direct math expressions after ">" const mathExpr = query.slice(1).trim() const evaluation = evaluate(mathExpr) - + if (evaluation.isValid) { results.push(createEntry(mathExpr, "direct")) } diff --git a/Modules/AppLauncher/ClipboardHistory.qml b/Modules/AppLauncher/ClipboardHistory.qml index dfd4d8c..13e59c8 100644 --- a/Modules/AppLauncher/ClipboardHistory.qml +++ b/Modules/AppLauncher/ClipboardHistory.qml @@ -58,8 +58,9 @@ QtObject { "type": 'text', "timestamp": clip.timestamp, "index": index, - "textData": textContent, // Store the text data for the execute function - "execute": function () { + "textData": textContent, + "execute"// Store the text data for the execute function + : function () { const text = this.textData || clip.content || clip Quickshell.clipboardText = String(text) copyText(String(text)) @@ -77,8 +78,7 @@ QtObject { "name": "No clipboard history", "content": "No matching clipboard entries found", "icon": "content_paste_off", - "execute": function () { - // Do nothing for empty state + "execute": function () {// Do nothing for empty state } } } @@ -122,8 +122,7 @@ QtObject { "name": ">clip", "content": "Clipboard history - browse and restore clipboard items", "icon": "content_paste", - "execute": function () { - // This should be handled by the parent component + "execute": function () {// This should be handled by the parent component } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index f800318..57fd876 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -14,12 +14,12 @@ import qs.Widgets NLoader { id: lockScreen - + // Log state changes to help debug lock screen issues onIsLoadedChanged: { Logger.log("LockScreen", "State changed - isLoaded:", isLoaded) } - + // Allow a small grace period after unlocking so the compositor releases the lock surfaces Timer { id: unloadAfterUnlockTimer diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml index 558b958..899ec77 100644 --- a/Services/ClipboardService.qml +++ b/Services/ClipboardService.qml @@ -101,15 +101,16 @@ Singleton { if (!exists) { // Normalize existing history and add the new image const normalizedHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - "type": 'text', - "content": item, - "timestamp": new Date().getTime() - 1000 // Make it slightly older - } - } - return item - }) + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime( + ) - 1000 // Make it slightly older + } + } + return item + }) root.history = [entry, ...normalizedHistory].slice(0, maxHistory) saveHistory() } @@ -133,7 +134,7 @@ Singleton { onExited: (exitCode, exitStatus) => { textProcess.isLoading = false - + if (exitCode === 0) { const content = String(stdout.text).trim() if (content && content.length > 0) { @@ -154,15 +155,16 @@ Singleton { if (!exists) { // Normalize existing history entries const normalizedHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - "type": 'text', - "content": item, - "timestamp": new Date().getTime() - 1000 // Make it slightly older - } - } - return item - }) + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime( + ) - 1000 // Make it slightly older + } + } + return item + }) root.history = [entry, ...normalizedHistory].slice(0, maxHistory) saveHistory() @@ -204,7 +206,7 @@ Singleton { try { // Ensure we don't exceed the maximum history limit const limitedHistory = root.history.slice(0, maxHistory) - + historyAdapter.history = limitedHistory historyAdapter.timestamp = Time.timestamp diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml index a37a452..3a9aedb 100644 --- a/Services/IdleInhibitorService.qml +++ b/Services/IdleInhibitorService.qml @@ -19,7 +19,7 @@ Singleton { Component.onCompleted: { Logger.log("IdleInhibitor", "Service started") detectStrategy() - + // Restore previous state from settings if (Settings.data.ui.idleInhibitorEnabled) { addInhibitor("manual", "Restored from previous session") @@ -37,18 +37,20 @@ Singleton { Logger.log("IdleInhibitor", "Using systemd-inhibit strategy") return } catch (e) { + // systemd-inhibit not found, try Wayland tools } - + try { var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]) strategy = "wayland" Logger.log("IdleInhibitor", "Using wayhibitor strategy") return } catch (e) { + // wayhibitor not found } - + Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback") strategy = "systemd" // Fallback to systemd even if not detected } @@ -84,9 +86,10 @@ Singleton { // Update the actual system inhibition function updateInhibition(newReason = reason) { const shouldInhibit = activeInhibitors.length > 0 - + if (shouldInhibit === isInhibited) { - return // No change needed + return + // No change needed } if (shouldInhibit) { @@ -99,7 +102,7 @@ Singleton { // Start system inhibition function startInhibition(newReason) { reason = newReason - + if (strategy === "systemd") { startSystemdInhibition() } else if (strategy === "wayland") { @@ -108,32 +111,28 @@ Singleton { Logger.warn("IdleInhibitor", "No inhibition strategy available") return } - + isInhibited = true Logger.log("IdleInhibitor", "Started inhibition:", reason) } // Stop system inhibition function stopInhibition() { - if (!isInhibited) return - + if (!isInhibited) + return + if (inhibitorProcess.running) { inhibitorProcess.signal(15) // SIGTERM } - + isInhibited = false Logger.log("IdleInhibitor", "Stopped inhibition") } // Systemd inhibition using systemd-inhibit function startSystemdInhibition() { - inhibitorProcess.command = [ - "systemd-inhibit", - "--what=idle:sleep:handle-lid-switch", - "--why=" + reason, - "--mode=block", - "sleep", "infinity" - ] + inhibitorProcess.command = ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why=" + + reason, "--mode=block", "sleep", "infinity"] inhibitorProcess.running = true } @@ -147,15 +146,15 @@ Singleton { Process { id: inhibitorProcess running: false - - onExited: function(exitCode, exitStatus) { + + onExited: function (exitCode, exitStatus) { if (isInhibited) { Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode) isInhibited = false } } - - onStarted: function() { + + onStarted: function () { Logger.log("IdleInhibitor", "Inhibitor process started successfully") } } diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml index 88e448e..61de0a8 100644 --- a/Widgets/NImageCached.qml +++ b/Widgets/NImageCached.qml @@ -47,4 +47,4 @@ Image { }) } } -} \ No newline at end of file +} diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index fe9a6ff..77d1812 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -20,7 +20,7 @@ RowLayout { property bool enabled: true property bool hovering: false property int baseSize: Style.baseWidgetSize - + // Convenience properties for common naming property alias minimum: spinBox.from property alias maximum: spinBox.to @@ -55,10 +55,10 @@ RowLayout { // Value Rectangle { id: spinBoxContainer - - implicitWidth: 100 * scaling // Wider for better proportions - implicitHeight: (root.baseSize - 4) * scaling // Slightly shorter than toggle - radius: height * 0.5 // Fully rounded like toggle + + implicitWidth: 100 * scaling // Wider for better proportions + implicitHeight: (root.baseSize - 4) * scaling // Slightly shorter than toggle + radius: height * 0.5 // Fully rounded like toggle color: Color.mSurfaceVariant border.color: root.hovering ? Color.mPrimary : Color.mOutline border.width: 1 @@ -83,7 +83,7 @@ RowLayout { root.hovering = false root.exited() } - onWheel: function(wheel) { + onWheel: function (wheel) { if (wheel.angleDelta.y > 0 && spinBox.value < spinBox.to) { spinBox.increase() } else if (wheel.angleDelta.y < 0 && spinBox.value > spinBox.from) { @@ -95,29 +95,29 @@ RowLayout { // Decrease button (left) Rectangle { id: decreaseButton - width: parent.height * 0.8 // Make it circular + width: parent.height * 0.8 // Make it circular height: parent.height * 0.8 anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: parent.height * 0.1 - radius: width * 0.5 // Perfect circle + radius: width * 0.5 // Perfect circle color: decreaseArea.containsMouse ? Color.mPrimary : "transparent" opacity: root.enabled && spinBox.value > spinBox.from ? 1.0 : 0.3 - + Behavior on color { ColorAnimation { duration: Style.animationFast easing.type: Easing.InOutCubic } } - + NIcon { anchors.centerIn: parent text: "remove" font.pointSize: Style.fontSizeS * scaling color: decreaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary } - + MouseArea { id: decreaseArea anchors.fill: parent @@ -127,33 +127,33 @@ RowLayout { onClicked: spinBox.decrease() } } - + // Increase button (right) Rectangle { id: increaseButton - width: parent.height * 0.8 // Make it circular + width: parent.height * 0.8 // Make it circular height: parent.height * 0.8 anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: parent.height * 0.1 - radius: width * 0.5 // Perfect circle + radius: width * 0.5 // Perfect circle color: increaseArea.containsMouse ? Color.mPrimary : "transparent" opacity: root.enabled && spinBox.value < spinBox.to ? 1.0 : 0.3 - + Behavior on color { ColorAnimation { duration: Style.animationFast easing.type: Easing.InOutCubic } } - + NIcon { anchors.centerIn: parent text: "add" font.pointSize: Style.fontSizeS * scaling color: increaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary } - + MouseArea { id: increaseArea anchors.fill: parent @@ -163,7 +163,7 @@ RowLayout { onClicked: spinBox.increase() } } - + // Center value display SpinBox { id: spinBox @@ -172,23 +172,23 @@ RowLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: 4 * scaling height: parent.height - + background: Item {} up.indicator: Item {} down.indicator: Item {} - + font.pointSize: Style.fontSizeM * scaling font.family: Settings.data.ui.fontDefault - + from: 0 to: 100 stepSize: 1 - editable: false // Only use buttons/scroll + editable: false // Only use buttons/scroll enabled: root.enabled - + contentItem: Item { anchors.fill: parent - + NText { anchors.centerIn: parent text: root.prefix + spinBox.value + root.suffix From 778f93401070b9449bd07cd0012a3109a81a36b8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 11:40:25 -0400 Subject: [PATCH 26/35] applauncher --- Modules/AppLauncher/AppLauncher.qml | 759 ++++++++++++++-------------- 1 file changed, 379 insertions(+), 380 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 89d8ca6..1650dd6 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -12,247 +12,289 @@ import qs.Widgets import "../../Helpers/FuzzySort.js" as Fuzzysort - NPanel { - id: root - panelWidth: Math.min(700 * scaling, screen?.width * 0.75) - panelHeight: Math.min(550 * scaling, screen?.height * 0.8) - panelAnchorCentered: true +NPanel { + id: root + panelWidth: Math.min(700 * scaling, screen?.width * 0.75) + panelHeight: Math.min(550 * scaling, screen?.height * 0.8) + panelAnchorCentered: true + // Import modular components + Calculator { + id: calculator + } - // Import modular components - Calculator { - id: calculator + ClipboardHistory { + id: clipboardHistory + } + + // Properties + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + + // Refresh clipboard when user starts typing clipboard commands + onSearchTextChanged: { + if (searchText.startsWith(">clip")) { + clipboardHistory.refresh() + } + } + + // Main filtering logic + property var filteredEntries: { + Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + Logger.log("AppLauncher", "No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : "" + var results = [] + + // Handle special commands + if (query === ">") { + results.push({ + "isCommand": true, + "name": ">calc", + "content": "Calculator - evaluate mathematical expressions", + "icon": "calculate", + "execute": executeCalcCommand + }) + + results.push({ + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": executeClipCommand + }) + + return results + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + return clipboardHistory.processQuery(query) + } + + // Handle calculator + if (query.startsWith(">calc")) { + return calculator.processQuery(query, "calc") + } + + // Handle direct math expressions after ">" + if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) { + const mathResults = calculator.processQuery(query, "direct") + if (mathResults.length > 0) { + return mathResults } + // If math evaluation fails, fall through to regular search + } - ClipboardHistory { - id: clipboardHistory + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + })) + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + "keys": ["name", "comment", "genericName"] + }) + results = results.concat(fuzzyResults.map(function (r) { + return r.obj + })) + } + + Logger.log("AppLauncher", "Filtered entries:", results.length) + return results + } + + // Command execution functions + function executeCalcCommand() { + searchText = ">calc " + searchInput.cursorPosition = searchText.length + } + + function executeClipCommand() { + searchText = ">clip " + searchInput.cursorPosition = searchText.length + } + + // Navigation functions + function selectNext() { + if (filteredEntries.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) + } + } + + function selectPrev() { + if (filteredEntries.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0) + } + } + + function activateSelected() { + if (filteredEntries.length === 0) + return + + var modelData = filteredEntries[selectedIndex] + if (modelData && modelData.execute) { + if (modelData.isCommand) { + modelData.execute() + return + } else { + modelData.execute() } + root.close() + } + } - // Properties - property var desktopEntries: DesktopEntries.applications.values - property string searchText: "" - property int selectedIndex: 0 + Component.onCompleted: { + Logger.log("AppLauncher", "Component completed") + Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') + if (typeof DesktopEntries !== 'undefined') { + Logger.log("AppLauncher", "DesktopEntries.entries:", + DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + } + // Start clipboard refresh immediately on open + clipboardHistory.refresh() + } - // Refresh clipboard when user starts typing clipboard commands - onSearchTextChanged: { - if (searchText.startsWith(">clip")) { - clipboardHistory.refresh() - } + // Main content container + panelContent: Rectangle { + + // Subtle gradient background + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(Color.mSurface, 1.02) } - - // Main filtering logic - property var filteredEntries: { - Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) - if (!desktopEntries || desktopEntries.length === 0) { - Logger.log("AppLauncher", "No desktop entries available") - return [] - } - - // Filter out entries that shouldn't be displayed - var visibleEntries = desktopEntries.filter(entry => { - if (!entry || entry.noDisplay) { - return false - } - return true - }) - - Logger.log("AppLauncher", "Visible entries:", visibleEntries.length) - - var query = searchText ? searchText.toLowerCase() : "" - var results = [] - - // Handle special commands - if (query === ">") { - results.push({ - "isCommand": true, - "name": ">calc", - "content": "Calculator - evaluate mathematical expressions", - "icon": "calculate", - "execute": executeCalcCommand - }) - - results.push({ - "isCommand": true, - "name": ">clip", - "content": "Clipboard history - browse and restore clipboard items", - "icon": "content_paste", - "execute": executeClipCommand - }) - - return results - } - - // Handle clipboard history - if (query.startsWith(">clip")) { - return clipboardHistory.processQuery(query) - } - - // Handle calculator - if (query.startsWith(">calc")) { - return calculator.processQuery(query, "calc") - } - - // Handle direct math expressions after ">" - if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) { - const mathResults = calculator.processQuery(query, "direct") - if (mathResults.length > 0) { - return mathResults - } - // If math evaluation fails, fall through to regular search - } - - // Regular app search - if (!query) { - results = results.concat(visibleEntries.sort(function (a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - })) - } else { - var fuzzyResults = Fuzzysort.go(query, visibleEntries, { - "keys": ["name", "comment", "genericName"] - }) - results = results.concat(fuzzyResults.map(function (r) { - return r.obj - })) - } - - Logger.log("AppLauncher", "Filtered entries:", results.length) - return results + GradientStop { + position: 1.0 + color: Qt.darker(Color.mSurface, 1.1) } + } - // Command execution functions - function executeCalcCommand() { - searchText = ">calc " - searchInput.cursorPosition = searchText.length - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling - function executeClipCommand() { - searchText = ">clip " - searchInput.cursorPosition = searchText.length - } + // Search bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.barHeight * scaling + Layout.bottomMargin: Style.marginM * scaling + radius: Style.radiusM * scaling + color: Color.mSurface + border.color: searchInput.activeFocus ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling) - // Navigation functions - function selectNext() { - if (filteredEntries.length > 0) { - selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) - } - } - - function selectPrev() { - if (filteredEntries.length > 0) { - selectedIndex = Math.max(selectedIndex - 1, 0) - } - } - - function activateSelected() { - if (filteredEntries.length === 0) - return - - var modelData = filteredEntries[selectedIndex] - if (modelData && modelData.execute) { - if (modelData.isCommand) { - modelData.execute() - return - } else { - modelData.execute() - } - root.close() - } - } - - Component.onCompleted: { - Logger.log("AppLauncher", "Component completed") - Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') - if (typeof DesktopEntries !== 'undefined') { - Logger.log("AppLauncher", "DesktopEntries.entries:", - DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') - } - // Start clipboard refresh immediately on open - clipboardHistory.refresh() - } - - // Main content container - panelContent: Rectangle { - - // Subtle gradient background - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.lighter(Color.mSurface, 1.02) - } - GradientStop { - position: 1.0 - color: Qt.darker(Color.mSurface, 1.1) - } - } - - ColumnLayout { + Item { anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling + anchors.margins: Style.marginM * scaling - // Search bar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.barHeight * scaling - Layout.bottomMargin: Style.marginM * scaling + NIcon { + id: searchIcon + text: "search" + font.pointSize: Style.fontSizeXL * scaling + color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + TextField { + id: searchInput + placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..." + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurfaceVariant + background: null + font.pointSize: Style.fontSizeL * scaling + anchors.left: searchIcon.right + anchors.leftMargin: Style.marginS * scaling + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + onTextChanged: { + searchText = text + // Defer selectedIndex reset to avoid binding loops + Qt.callLater(() => selectedIndex = 0) + } + selectedTextColor: Color.mOnSurface + selectionColor: Color.mPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + contentItem.cursorColor = Color.mOnSurface + contentItem.verticalAlignment = TextInput.AlignVCenter + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() + }) + } + onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface + + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: root.close() + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Applications list + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ListView { + id: appsList + anchors.fill: parent + spacing: Style.marginXXS * scaling + model: filteredEntries + currentIndex: selectedIndex + + delegate: Rectangle { + width: appsList.width - Style.marginS * scaling + height: 65 * scaling radius: Style.radiusM * scaling - color: Color.mSurface - border.color: searchInput.activeFocus ? Color.mPrimary : Color.mOutline - border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling) + property bool isSelected: index === selectedIndex + color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurface + border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent + border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderM * scaling : 0) - Item { - anchors.fill: parent - anchors.margins: Style.marginM * scaling - - NIcon { - id: searchIcon - text: "search" - font.pointSize: Style.fontSizeXL * scaling - color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - } - - TextField { - id: searchInput - placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..." - color: Color.mOnSurface - placeholderTextColor: Color.mOnSurfaceVariant - background: null - font.pointSize: Style.fontSizeL * scaling - anchors.left: searchIcon.right - anchors.leftMargin: Style.marginS * scaling - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - onTextChanged: { - searchText = text - // Defer selectedIndex reset to avoid binding loops - Qt.callLater(() => selectedIndex = 0) - } - selectedTextColor: Color.mOnSurface - selectionColor: Color.mPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - font.bold: true - Component.onCompleted: { - contentItem.cursorColor = Color.mOnSurface - contentItem.verticalAlignment = TextInput.AlignVCenter - // Focus the search bar by default - Qt.callLater(() => { - searchInput.forceActiveFocus() - }) - } - onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface - - Keys.onDownPressed: selectNext() - Keys.onUpPressed: selectPrev() - Keys.onEnterPressed: activateSelected() - Keys.onReturnPressed: activateSelected() - Keys.onEscapePressed: root.close() + Behavior on color { + ColorAnimation { + duration: Style.animationFast } } @@ -267,179 +309,136 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort duration: Style.animationFast } } - } - // Applications list - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - ListView { - id: appsList + RowLayout { anchors.fill: parent - spacing: Style.marginXXS * scaling - model: filteredEntries - currentIndex: selectedIndex + anchors.margins: Style.marginM * scaling + spacing: Style.marginM * scaling - delegate: Rectangle { - width: appsList.width - Style.marginS * scaling - height: 65 * scaling - radius: Style.radiusM * scaling - property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurface - border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent - border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderM * scaling : 0) + // App icon with background + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling + Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling + radius: Style.radiusS * scaling + color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurfaceVariant + property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) + || (iconImg.status === Image.Ready && iconImg.source !== "" + && iconImg.status !== Image.Error && iconImg.source !== "") + visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode + + // Clipboard image display + Image { + id: clipboardImage + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + IconImage { + id: iconImg + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + asynchronous: true + source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) + && modelData.type !== 'image' + } + + // Fallback icon container + Rectangle { + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + radius: Style.radiusXS * scaling + color: Color.mPrimary + opacity: Style.opacityMedium + visible: !parent.iconLoaded + } + + NText { + anchors.centerIn: parent + visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Font.Bold + color: Color.mPrimary + } Behavior on color { ColorAnimation { duration: Style.animationFast } } + } - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS * scaling + + NText { + text: modelData.name || "Unknown" + font.pointSize: Style.fontSizeL * scaling + font.weight: Font.Bold + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true } - Behavior on border.width { - NumberAnimation { - duration: Style.animationFast - } - } - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginM * scaling - spacing: Style.marginM * scaling - - // App icon with background - Rectangle { - Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling - Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling - radius: Style.radiusS * scaling - color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurfaceVariant - property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) - || (iconImg.status === Image.Ready && iconImg.source !== "" - && iconImg.status !== Image.Error && iconImg.source !== "") - visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode - - // Clipboard image display - Image { - id: clipboardImage - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - visible: modelData.type === 'image' - source: modelData.data || "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - } - - IconImage { - id: iconImg - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - asynchronous: true - source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") - visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand - || parent.iconLoaded) && modelData.type !== 'image' - } - - // Fallback icon container - Rectangle { - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - radius: Style.radiusXS * scaling - color: Color.mPrimary - opacity: Style.opacityMedium - visible: !parent.iconLoaded - } - - NText { - anchors.centerIn: parent - visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard - || modelData.isCommand) - text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Font.Bold - color: Color.mPrimary - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - } - - // App info - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginXXS * scaling - - NText { - text: modelData.name || "Unknown" - font.pointSize: Style.fontSizeL * scaling - font.weight: Font.Bold - color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface - elide: Text.ElideRight - Layout.fillWidth: true - } - - NText { - text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") - font.pointSize: Style.fontSizeM * scaling - color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface - elide: Text.ElideRight - Layout.fillWidth: true - visible: text !== "" - } - } - } - - MouseArea { - id: appCardArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - selectedIndex = index - activateSelected() - } + NText { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") + font.pointSize: Style.fontSizeM * scaling + color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" } } } - } - // No results message - NText { - text: searchText.trim() !== "" ? "No applications found" : "No applications available" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - visible: filteredEntries.length === 0 - } + MouseArea { + id: appCardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor - // Results count - NText { - text: searchText.startsWith( - ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length - !== 1 ? 's' : ''}` : searchText.startsWith( - ">calc") ? `${filteredEntries.length} result${filteredEntries.length - !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length - !== 1 ? 's' : ''}` - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - Layout.fillWidth: true - visible: searchText.trim() !== "" + onClicked: { + selectedIndex = index + activateSelected() + } + } } } } + + // No results message + NText { + text: searchText.trim() !== "" ? "No applications found" : "No applications available" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: filteredEntries.length === 0 + } + + // Results count + NText { + text: searchText.startsWith( + ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length + !== 1 ? 's' : ''}` : searchText.startsWith( + ">calc") ? `${filteredEntries.length} result${filteredEntries.length + !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length + !== 1 ? 's' : ''}` + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + visible: searchText.trim() !== "" + } } + } +} From fe3818d531651d82bf8151f303be587507b8a5f4 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 11:40:33 -0400 Subject: [PATCH 27/35] powermenu wip --- Modules/SidePanel/Cards/ProfileCard.qml | 5 +- Modules/SidePanel/PowerMenu.qml | 164 +++++++++++++----------- 2 files changed, 90 insertions(+), 79 deletions(-) diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 803565a..5e38f53 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -70,7 +70,7 @@ NBox { icon: "power_settings_new" tooltipText: "Power Menu" onClicked: { - powerMenu.show() + powerMenu.open(screen) } } } @@ -78,9 +78,6 @@ NBox { PowerMenu { id: powerMenu - // TBC - // anchors.top: powerButton.bottom - // anchors.right: powerButton.right } // ---------------------------------- diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 84ee679..83dcbaa 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -4,26 +4,94 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Widgets +import Quickshell.Wayland import qs.Commons import qs.Services import qs.Widgets import qs.Modules.LockScreen -NPanel { - id: powerMenu +PanelWindow { + id: root + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true visible: false + color: "#3300FF00" + screen: screen + + onVisibleChanged: { + + if (visible) { + console.log("Oh Yeah") + Qt.callLater(() => forceActiveFocus()) + + } + } + + function open() { + visible = true + } + + function close() { + visible = false + } + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + // ---------------------------------- + // System functions + function logout() { + CompositorService.logout() + } + + function suspend() { + suspendProcess.running = true + } + + function shutdown() { + shutdownProcess.running = true + } + + function reboot() { + rebootProcess.running = true + } + + Process { + id: shutdownProcess + + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + + command: ["systemctl", "suspend"] + running: false + } + + Process { + id: logoutProcess + + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + running: false + } property var entriesCount: 5 property var entryHeight: Style.baseWidgetSize * scaling - // Anchors will be set by the parent component - function show() { - visible = true - } - - function hide() { - visible = false - } Rectangle { width: 160 * scaling @@ -41,13 +109,10 @@ NPanel { anchors.rightMargin: Style.marginL * scaling anchors.topMargin: 86 * scaling - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - onClicked: { - - } - } + Component.onCompleted: { + console.log("oncompleted") + Qt.callLater(() => forceActiveFocus()) + } ColumnLayout { anchors.fill: parent @@ -102,9 +167,9 @@ NPanel { cursorShape: Qt.PointingHandCursor onClicked: { Logger.log("PowerMenu", "Lock screen requested") - // Lock the screen - lockScreen.isLoaded = true - powerMenu.visible = false + // // Lock the screen + // lockScreen.isLoaded = true + // root.close() } } } @@ -157,7 +222,7 @@ NPanel { cursorShape: Qt.PointingHandCursor onClicked: { suspend() - powerMenu.visible = false + root.close() } } } @@ -210,7 +275,7 @@ NPanel { cursorShape: Qt.PointingHandCursor onClicked: { reboot() - powerMenu.visible = false + root.close() } } } @@ -263,7 +328,7 @@ NPanel { cursorShape: Qt.PointingHandCursor onClicked: { logout() - powerMenu.visible = false + root.close() } } } @@ -316,61 +381,10 @@ NPanel { cursorShape: Qt.PointingHandCursor onClicked: { shutdown() - powerMenu.visible = false + root.close() } } } } } - - // ---------------------------------- - // System functions - function logout() { - CompositorService.logout() - } - - function suspend() { - suspendProcess.running = true - } - - function shutdown() { - shutdownProcess.running = true - } - - function reboot() { - rebootProcess.running = true - } - - Process { - id: shutdownProcess - - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - - command: ["systemctl", "suspend"] - running: false - } - - Process { - id: logoutProcess - - command: ["loginctl", "terminate-user", Quickshell.env("USER")] - running: false - } - - // LockScreen instance - LockScreen { - id: lockScreen - } } From fc9a53fb0de0c68c56d7b0cc29f20dce8f8db5e2 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 12:05:53 -0400 Subject: [PATCH 28/35] AppLauncher focus fix --- Modules/AppLauncher/AppLauncher.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 1650dd6..2d2000c 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -238,14 +238,15 @@ NPanel { bottomPadding: 0 font.bold: true Component.onCompleted: { - contentItem.cursorColor = Color.mOnSurface - contentItem.verticalAlignment = TextInput.AlignVCenter + + // contentItem.cursorColor = Color.mOnSurface + // contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } - onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface + //onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() From a91c0978c07589e896586052aea5076ce65661bc Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 12:25:27 -0400 Subject: [PATCH 29/35] Wip Powermenu --- Modules/SidePanel/Cards/ProfileCard.qml | 14 ++++-- Modules/SidePanel/PowerMenu.qml | 66 ++++++++++++++----------- shell.qml | 4 ++ 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 5e38f53..c44311c 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -71,14 +71,22 @@ NBox { tooltipText: "Power Menu" onClicked: { powerMenu.open(screen) + sidePanel.close() + } + } + + NIconButton { + id: closeButton + icon: "close" + tooltipText: "Close Side Panel" + onClicked: { + sidePanel.close() } } } } - PowerMenu { - id: powerMenu - } + // ---------------------------------- // Uptime diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index 83dcbaa..d45ea40 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -12,6 +12,9 @@ import qs.Modules.LockScreen PanelWindow { id: root + readonly property real scaling: ScalingService.scale(screen) + + anchors.top: true anchors.left: true anchors.right: true @@ -21,13 +24,12 @@ PanelWindow { screen: screen onVisibleChanged: { - - if (visible) { - console.log("Oh Yeah") - Qt.callLater(() => forceActiveFocus()) - - } + if (visible) { + console.log("Oh Yeah") + // Focus the menu rectangle instead of root + menuRect.forceActiveFocus() } + } function open() { visible = true @@ -37,11 +39,14 @@ PanelWindow { visible = false } - // Clicking outside of the rectangle to close - MouseArea { - anchors.fill: parent - onClicked: root.close() - } + // // Clicking outside of the rectangle to close + // MouseArea { + // anchors.fill: parent + // onClicked: root.close() + // // Prevent this MouseArea from interfering with child elements + // propagateComposedEvents: true + // z: -1 // Put it behind the menu + // } // ---------------------------------- // System functions @@ -63,28 +68,24 @@ PanelWindow { Process { id: shutdownProcess - command: ["shutdown", "-h", "now"] running: false } Process { id: rebootProcess - command: ["reboot"] running: false } Process { id: suspendProcess - command: ["systemctl", "suspend"] running: false } Process { id: logoutProcess - command: ["loginctl", "terminate-user", Quickshell.env("USER")] running: false } @@ -92,27 +93,30 @@ PanelWindow { property var entriesCount: 5 property var entryHeight: Style.baseWidgetSize * scaling - Rectangle { + id: menuRect width: 160 * scaling height: (entryHeight * entriesCount) + (Style.marginS * entriesCount * scaling) radius: Style.radiusM * scaling border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) color: Color.mSurface - visible: true z: 9999 + + // Add focus properties + focus: true + activeFocusOnTab: true anchors.top: parent.top anchors.right: parent.right anchors.rightMargin: Style.marginL * scaling anchors.topMargin: 86 * scaling - Component.onCompleted: { - console.log("oncompleted") - Qt.callLater(() => forceActiveFocus()) - } + Component.onCompleted: { + console.log("oncompleted") + forceActiveFocus() + } ColumnLayout { anchors.fill: parent @@ -161,15 +165,17 @@ PanelWindow { MouseArea { id: lockButtonArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + // Add acceptedButtons to ensure proper click handling + acceptedButtons: Qt.LeftButton + onClicked: { Logger.log("PowerMenu", "Lock screen requested") // // Lock the screen // lockScreen.isLoaded = true - // root.close() + root.close() } } } @@ -216,10 +222,11 @@ PanelWindow { MouseArea { id: suspendButtonArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onClicked: { suspend() root.close() @@ -269,10 +276,11 @@ PanelWindow { MouseArea { id: rebootButtonArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onClicked: { reboot() root.close() @@ -322,10 +330,11 @@ PanelWindow { MouseArea { id: logoutButtonArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onClicked: { logout() root.close() @@ -375,10 +384,11 @@ PanelWindow { MouseArea { id: shutdownButtonArea - anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onClicked: { shutdown() root.close() @@ -387,4 +397,4 @@ PanelWindow { } } } -} +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 8554c71..837c6e2 100644 --- a/shell.qml +++ b/shell.qml @@ -65,6 +65,10 @@ ShellRoot { id: lockScreen } + PowerMenu { + id: powerMenu + } + ToastManager {} IPCManager {} From 1fc717f35499a8e8f59b69070a133628abac2719 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 12:33:00 -0400 Subject: [PATCH 30/35] Power menu is out of SidePanel --- Modules/SidePanel/PowerMenu.qml | 77 ++++++--------------------------- 1 file changed, 13 insertions(+), 64 deletions(-) diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml index d45ea40..7b91eee 100644 --- a/Modules/SidePanel/PowerMenu.qml +++ b/Modules/SidePanel/PowerMenu.qml @@ -10,45 +10,23 @@ import qs.Services import qs.Widgets import qs.Modules.LockScreen -PanelWindow { + + +NPanel { id: root - readonly property real scaling: ScalingService.scale(screen) + panelWidth: 500 * scaling + panelHeight: 300 * scaling + panelAnchorCentered: true - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - visible: false - color: "#3300FF00" - screen: screen - onVisibleChanged: { - if (visible) { - console.log("Oh Yeah") - // Focus the menu rectangle instead of root - menuRect.forceActiveFocus() - } - } + property var entriesCount: 5 + property var entryHeight: Style.baseWidgetSize * scaling - function open() { - visible = true - } + panelContent: Rectangle { + color: Color.transparent - function close() { - visible = false - } - - // // Clicking outside of the rectangle to close - // MouseArea { - // anchors.fill: parent - // onClicked: root.close() - // // Prevent this MouseArea from interfering with child elements - // propagateComposedEvents: true - // z: -1 // Put it behind the menu - // } - - // ---------------------------------- + // ---------------------------------- // System functions function logout() { CompositorService.logout() @@ -90,34 +68,6 @@ PanelWindow { running: false } - property var entriesCount: 5 - property var entryHeight: Style.baseWidgetSize * scaling - - Rectangle { - id: menuRect - width: 160 * scaling - height: (entryHeight * entriesCount) + (Style.marginS * entriesCount * scaling) - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - color: Color.mSurface - visible: true - z: 9999 - - // Add focus properties - focus: true - activeFocusOnTab: true - - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: Style.marginL * scaling - anchors.topMargin: 86 * scaling - - Component.onCompleted: { - console.log("oncompleted") - forceActiveFocus() - } - ColumnLayout { anchors.fill: parent anchors.margins: Style.marginS * scaling @@ -172,9 +122,8 @@ PanelWindow { acceptedButtons: Qt.LeftButton onClicked: { - Logger.log("PowerMenu", "Lock screen requested") - // // Lock the screen - // lockScreen.isLoaded = true + // Lock the screen + lockScreen.isLoaded = true root.close() } } From 6538ef1459a9143bce9a03d7d37c97ea10736e63 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Wed, 20 Aug 2025 20:04:39 +0200 Subject: [PATCH 31/35] Make PowerPanel look nice --- Modules/PowerPanel/PowerPanel.qml | 347 +++++++++++++++++++++++ Modules/SidePanel/Cards/ProfileCard.qml | 2 +- Modules/SidePanel/PowerMenu.qml | 349 ------------------------ shell.qml | 5 +- 4 files changed, 351 insertions(+), 352 deletions(-) create mode 100644 Modules/PowerPanel/PowerPanel.qml delete mode 100644 Modules/SidePanel/PowerMenu.qml diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml new file mode 100644 index 0000000..cf52186 --- /dev/null +++ b/Modules/PowerPanel/PowerPanel.qml @@ -0,0 +1,347 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NPanel { + id: root + + panelWidth: 420 * scaling + panelHeight: 370 * scaling + panelAnchorCentered: true + + // Timer properties + property int timerDuration: 5000 // 5 seconds + property string pendingAction: "" + property bool timerActive: false + property int timeRemaining: 0 + + // Cancel timer when panel is closing + onClosed: { + cancelTimer() + } + + // Timer management + function startTimer(action) { + if (timerActive && pendingAction === action) { + // Second click - execute immediately + executeAction(action) + return + } + + pendingAction = action + timeRemaining = timerDuration + timerActive = true + countdownTimer.start() + } + + function cancelTimer() { + timerActive = false + pendingAction = "" + timeRemaining = 0 + countdownTimer.stop() + } + + function executeAction(action) { + // Stop timer but don't reset other properties yet + countdownTimer.stop() + + switch(action) { + case "lock": + // Access lockScreen directly like IPCManager does + if (!lockScreen.isLoaded) { + lockScreen.isLoaded = true + } + break + case "suspend": + suspendProcess.running = true + break + case "reboot": + rebootProcess.running = true + break + case "logout": + CompositorService.logout() + break + case "shutdown": + shutdownProcess.running = true + break + } + + // Reset timer state and close panel + cancelTimer() + root.close() + } + + // System processes + Process { + id: shutdownProcess + command: ["shutdown", "-h", "now"] + running: false + } + + Process { + id: rebootProcess + command: ["reboot"] + running: false + } + + Process { + id: suspendProcess + command: ["systemctl", "suspend"] + running: false + } + + // Countdown timer + Timer { + id: countdownTimer + interval: 100 + repeat: true + onTriggered: { + timeRemaining -= interval + if (timeRemaining <= 0) { + executeAction(pendingAction) + } + } + } + + panelContent: Rectangle { + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.marginL * scaling + anchors.leftMargin: Style.marginL * scaling + anchors.rightMargin: Style.marginL * scaling + anchors.bottomMargin: Style.marginM * scaling + spacing: Style.marginS * scaling + + // Header with title and close button + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling + + NText { + text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)}s` : "Power Options" + font.weight: Style.fontWeightBold + font.pointSize: Style.fontSizeM * scaling + color: timerActive ? Color.mPrimary : Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + } + + Item { Layout.fillWidth: true } + + NIconButton { + icon: timerActive ? "block" : "close" + tooltipText: timerActive ? "Cancel Timer" : "Close" + Layout.alignment: Qt.AlignVCenter + colorBg: timerActive ? Color.applyOpacity(Color.mError, "20") : Color.transparent + colorFg: timerActive ? Color.mError : Color.mOnSurface + onClicked: { + if (timerActive) { + cancelTimer() + } else { + cancelTimer() + root.close() + } + } + } + } + + + + // Power options + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + // Lock Screen + PowerButton { + Layout.fillWidth: true + icon: "lock_outline" + title: "Lock Screen" + subtitle: "Lock your session" + onClicked: startTimer("lock") + pending: timerActive && pendingAction === "lock" + } + + // Suspend + PowerButton { + Layout.fillWidth: true + icon: "bedtime" + title: "Suspend" + subtitle: "Put the system to sleep" + onClicked: startTimer("suspend") + pending: timerActive && pendingAction === "suspend" + } + + // Reboot + PowerButton { + Layout.fillWidth: true + icon: "refresh" + title: "Reboot" + subtitle: "Restart the system" + onClicked: startTimer("reboot") + pending: timerActive && pendingAction === "reboot" + } + + // Logout + PowerButton { + Layout.fillWidth: true + icon: "exit_to_app" + title: "Logout" + subtitle: "End your session" + onClicked: startTimer("logout") + pending: timerActive && pendingAction === "logout" + } + + // Shutdown + PowerButton { + Layout.fillWidth: true + icon: "power_settings_new" + title: "Shutdown" + subtitle: "Turn off the system" + onClicked: startTimer("shutdown") + pending: timerActive && pendingAction === "shutdown" + isShutdown: true + } + } + + + } + } + + // Custom power button component + component PowerButton: Rectangle { + id: buttonRoot + + property string icon: "" + property string title: "" + property string subtitle: "" + property bool pending: false + property bool isShutdown: false + + signal clicked() + + height: Style.baseWidgetSize * 1.5 * scaling + radius: Style.radiusS * scaling + color: { + if (pending) return Color.applyOpacity(Color.mPrimary, "20") + if (mouseArea.containsMouse) return Color.mSurfaceVariant + return Color.transparent + } + + border.width: pending ? 2 * scaling : (mouseArea.containsMouse ? 1 * scaling : 0) + border.color: pending ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Item { + anchors.fill: parent + anchors.margins: Style.marginM * scaling + + // Icon on the left + NIcon { + id: iconElement + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: buttonRoot.icon + color: { + if (buttonRoot.pending) return Color.mPrimary + if (buttonRoot.isShutdown && mouseArea.containsMouse) return Color.mError + if (mouseArea.containsMouse) return Color.mPrimary + return Color.mOnSurface + } + font.pointSize: Style.fontSizeL * scaling + width: Style.baseWidgetSize * 0.6 * scaling + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Behavior on color { + ColorAnimation { duration: 150 } + } + } + + // Text content in the middle + Column { + anchors.left: iconElement.right + anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginM * scaling + anchors.rightMargin: pendingIndicator.visible ? Style.marginM * scaling : 0 + spacing: 2 * scaling + + NText { + text: buttonRoot.title + font.weight: Style.fontWeightMedium + font.pointSize: Style.fontSizeS * scaling + color: { + if (buttonRoot.pending) return Color.mPrimary + if (buttonRoot.isShutdown && mouseArea.containsMouse) return Color.mError + if (mouseArea.containsMouse) return Color.mPrimary + return Color.mOnSurface + } + + Behavior on color { + ColorAnimation { duration: 150 } + } + } + + NText { + text: { + if (buttonRoot.pending) { + return "Click again to execute immediately" + } + return buttonRoot.subtitle + } + font.pointSize: Style.fontSizeXS * scaling + color: { + if (buttonRoot.pending) return Color.mPrimary + return Color.mOnSurfaceVariant + } + opacity: 0.8 + wrapMode: Text.WordWrap + } + } + + // Pending indicator on the right + Rectangle { + id: pendingIndicator + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 24 * scaling + height: 24 * scaling + radius: 12 * scaling + color: Color.mPrimary + visible: buttonRoot.pending + + NText { + anchors.centerIn: parent + text: Math.ceil(timeRemaining / 1000) + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + } + } + } + + + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: buttonRoot.clicked() + } + } +} \ No newline at end of file diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index c44311c..0ad8dbc 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -70,7 +70,7 @@ NBox { icon: "power_settings_new" tooltipText: "Power Menu" onClicked: { - powerMenu.open(screen) + powerPanel.open(screen) sidePanel.close() } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml deleted file mode 100644 index 7b91eee..0000000 --- a/Modules/SidePanel/PowerMenu.qml +++ /dev/null @@ -1,349 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import Quickshell.Wayland -import qs.Commons -import qs.Services -import qs.Widgets -import qs.Modules.LockScreen - - - -NPanel { - id: root - - panelWidth: 500 * scaling - panelHeight: 300 * scaling - panelAnchorCentered: true - - - property var entriesCount: 5 - property var entryHeight: Style.baseWidgetSize * scaling - - panelContent: Rectangle { - color: Color.transparent - - // ---------------------------------- - // System functions - function logout() { - CompositorService.logout() - } - - function suspend() { - suspendProcess.running = true - } - - function shutdown() { - shutdownProcess.running = true - } - - function reboot() { - rebootProcess.running = true - } - - Process { - id: shutdownProcess - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - command: ["systemctl", "suspend"] - running: false - } - - Process { - id: logoutProcess - command: ["loginctl", "terminate-user", Quickshell.env("USER")] - running: false - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * scaling - - // -------------- - // Lock - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: entryHeight - radius: Style.radiusS * scaling - color: lockButtonArea.containsMouse ? Color.mTertiary : Color.transparent - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - - Row { - id: lockRow - spacing: Style.marginS * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - NIcon { - text: "lock_outline" - color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - NText { - text: "Lock Screen" - color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - font.pointSize: Style.fontSizeS * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: lockButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - // Add acceptedButtons to ensure proper click handling - acceptedButtons: Qt.LeftButton - - onClicked: { - // Lock the screen - lockScreen.isLoaded = true - root.close() - } - } - } - - // -------------- - // Suspend - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: entryHeight - radius: Style.radiusS * scaling - color: suspendButtonArea.containsMouse ? Color.mTertiary : Color.transparent - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - - Row { - id: suspendRow - spacing: Style.marginS * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - NIcon { - text: "bedtime" - color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - NText { - text: "Suspend" - color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - font.pointSize: Style.fontSizeS * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: suspendButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton - - onClicked: { - suspend() - root.close() - } - } - } - - // -------------- - // Reboot - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: entryHeight - radius: Style.radiusS * scaling - color: rebootButtonArea.containsMouse ? Color.mTertiary : Color.transparent - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - - Row { - id: rebootRow - spacing: Style.marginS * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - NIcon { - text: "refresh" - color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - NText { - text: "Reboot" - color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - font.pointSize: Style.fontSizeS * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: rebootButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton - - onClicked: { - reboot() - root.close() - } - } - } - - // -------------- - // Logout - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: entryHeight - radius: Style.radiusS * scaling - color: logoutButtonArea.containsMouse ? Color.mTertiary : Color.transparent - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - - Row { - id: logoutRow - spacing: Style.marginS * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - NIcon { - text: "exit_to_app" - color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - NText { - text: "Logout" - color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - font.pointSize: Style.fontSizeS * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: logoutButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton - - onClicked: { - logout() - root.close() - } - } - } - - // -------------- - // Shutdown - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: entryHeight - radius: Style.radiusS * scaling - color: shutdownButtonArea.containsMouse ? Color.mTertiary : Color.transparent - - Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - - Row { - id: shutdownRow - spacing: Style.marginS * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - NIcon { - text: "power_settings_new" - color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - - NText { - text: "Shutdown" - color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface - font.pointSize: Style.fontSizeS * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 * scaling - } - } - } - - MouseArea { - id: shutdownButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton - - onClicked: { - shutdown() - root.close() - } - } - } - } - } -} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 837c6e2..6bfe060 100644 --- a/shell.qml +++ b/shell.qml @@ -22,6 +22,7 @@ import qs.Modules.IPC import qs.Modules.LockScreen import qs.Modules.Notification import qs.Modules.SettingsPanel +import qs.Modules.PowerPanel import qs.Modules.SidePanel import qs.Modules.Toast @@ -65,8 +66,8 @@ ShellRoot { id: lockScreen } - PowerMenu { - id: powerMenu + PowerPanel { + id: powerPanel } ToastManager {} From 3de8c0af130bdb0ea6e06cc3d4fc9a182a7ad058 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 19:34:33 -0400 Subject: [PATCH 32/35] PowerPanel polishing --- Assets/ColorScheme/Rosepine.json | 2 +- Modules/AppLauncher/AppLauncher.qml | 5 - Modules/PowerPanel/PowerPanel.qml | 170 ++++++++++++------------ Modules/SidePanel/Cards/ProfileCard.qml | 2 - Services/CompositorService.qml | 42 +++--- 5 files changed, 112 insertions(+), 109 deletions(-) diff --git a/Assets/ColorScheme/Rosepine.json b/Assets/ColorScheme/Rosepine.json index 73ce3a3..90a9630 100644 --- a/Assets/ColorScheme/Rosepine.json +++ b/Assets/ColorScheme/Rosepine.json @@ -5,7 +5,7 @@ "mSecondary": "#9ccfd8", "mOnSecondary": "#191724", "mTertiary": "#31748f", - "mOnTertiary": "#e0def4", + "mOnTertiary": "#191724", "mError": "#eb6f92", "mOnError": "#1f1d2e", "mSurface": "#191724", diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index 2d2000c..6567430 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -238,16 +238,11 @@ NPanel { bottomPadding: 0 font.bold: true Component.onCompleted: { - - // contentItem.cursorColor = Color.mOnSurface - // contentItem.verticalAlignment = TextInput.AlignVCenter // Focus the search bar by default Qt.callLater(() => { searchInput.forceActiveFocus() }) } - //onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface - Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() Keys.onEnterPressed: activateSelected() diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index cf52186..d733186 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -12,12 +12,12 @@ import qs.Widgets NPanel { id: root - panelWidth: 420 * scaling - panelHeight: 370 * scaling + panelWidth: 440 * scaling + panelHeight: 380 * scaling panelAnchorCentered: true // Timer properties - property int timerDuration: 5000 // 5 seconds + property int timerDuration: 9000 // 9 seconds property string pendingAction: "" property bool timerActive: false property int timeRemaining: 0 @@ -34,7 +34,7 @@ NPanel { executeAction(action) return } - + pendingAction = action timeRemaining = timerDuration timerActive = true @@ -51,52 +51,33 @@ NPanel { function executeAction(action) { // Stop timer but don't reset other properties yet countdownTimer.stop() - - switch(action) { - case "lock": - // Access lockScreen directly like IPCManager does - if (!lockScreen.isLoaded) { - lockScreen.isLoaded = true - } - break - case "suspend": - suspendProcess.running = true - break - case "reboot": - rebootProcess.running = true - break - case "logout": - CompositorService.logout() - break - case "shutdown": - shutdownProcess.running = true - break + + switch (action) { + case "lock": + // Access lockScreen directly like IPCManager does + if (!lockScreen.isLoaded) { + lockScreen.isLoaded = true + } + break + case "suspend": + CompositorService.suspend() + break + case "reboot": + CompositorService.reboot() + break + case "logout": + CompositorService.logout() + break + case "shutdown": + CompositorService.shutdown() + break } - + // Reset timer state and close panel cancelTimer() root.close() } - // System processes - Process { - id: shutdownProcess - command: ["shutdown", "-h", "now"] - running: false - } - - Process { - id: rebootProcess - command: ["reboot"] - running: false - } - - Process { - id: suspendProcess - command: ["systemctl", "suspend"] - running: false - } - // Countdown timer Timer { id: countdownTimer @@ -127,18 +108,21 @@ NPanel { Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling NText { - text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)}s` : "Power Options" + text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil( + timeRemaining / 1000)} seconds...` : "Power Options" font.weight: Style.fontWeightBold - font.pointSize: Style.fontSizeM * scaling + font.pointSize: Style.fontSizeL * scaling color: timerActive ? Color.mPrimary : Color.mOnSurface Layout.alignment: Qt.AlignVCenter verticalAlignment: Text.AlignVCenter } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } NIconButton { - icon: timerActive ? "block" : "close" + icon: timerActive ? "back_hand" : "close" tooltipText: timerActive ? "Cancel Timer" : "Close" Layout.alignment: Qt.AlignVCenter colorBg: timerActive ? Color.applyOpacity(Color.mError, "20") : Color.transparent @@ -154,8 +138,6 @@ NPanel { } } - - // Power options ColumnLayout { Layout.fillWidth: true @@ -165,7 +147,7 @@ NPanel { PowerButton { Layout.fillWidth: true icon: "lock_outline" - title: "Lock Screen" + title: "Lock" subtitle: "Lock your session" onClicked: startTimer("lock") pending: timerActive && pendingAction === "lock" @@ -212,41 +194,43 @@ NPanel { isShutdown: true } } - - } } // Custom power button component component PowerButton: Rectangle { id: buttonRoot - + property string icon: "" property string title: "" property string subtitle: "" property bool pending: false property bool isShutdown: false - - signal clicked() - height: Style.baseWidgetSize * 1.5 * scaling + signal clicked + + height: Style.baseWidgetSize * 1.6 * scaling radius: Style.radiusS * scaling color: { - if (pending) return Color.applyOpacity(Color.mPrimary, "20") - if (mouseArea.containsMouse) return Color.mSurfaceVariant + if (pending) + return Color.applyOpacity(Color.mPrimary, "20") + if (mouseArea.containsMouse) + return Color.mTertiary return Color.transparent } - - border.width: pending ? 2 * scaling : (mouseArea.containsMouse ? 1 * scaling : 0) + + border.width: pending ? Math.max(Style.borderM * scaling) : 0 border.color: pending ? Color.mPrimary : Color.mOutline Behavior on color { - ColorAnimation { duration: 150 } + ColorAnimation { + duration: Style.animationFast + } } Item { anchors.fill: parent - anchors.margins: Style.marginM * scaling + anchors.margins: Style.marginL * scaling // Icon on the left NIcon { @@ -255,18 +239,24 @@ NPanel { anchors.verticalCenter: parent.verticalCenter text: buttonRoot.icon color: { - if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) return Color.mPrimary + + if (buttonRoot.pending) + return Color.mPrimary + if (buttonRoot.isShutdown && !mouseArea.containsMouse) + return Color.mError + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurface } - font.pointSize: Style.fontSizeL * scaling + font.pointSize: Style.fontSizeXXXL * scaling width: Style.baseWidgetSize * 0.6 * scaling horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - + Behavior on color { - ColorAnimation { duration: 150 } + ColorAnimation { + duration: Style.animationFast + } } } @@ -275,23 +265,28 @@ NPanel { anchors.left: iconElement.right anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginXL * scaling anchors.rightMargin: pendingIndicator.visible ? Style.marginM * scaling : 0 - spacing: 2 * scaling + spacing: 0 NText { text: buttonRoot.title font.weight: Style.fontWeightMedium - font.pointSize: Style.fontSizeS * scaling + font.pointSize: Style.fontSizeM * scaling color: { - if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) return Color.mPrimary + if (buttonRoot.pending) + return Color.mPrimary + if (buttonRoot.isShutdown && !mouseArea.containsMouse) + return Color.mError + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurface } - + Behavior on color { - ColorAnimation { duration: 150 } + ColorAnimation { + duration: Style.animationFast + } } } @@ -304,10 +299,15 @@ NPanel { } font.pointSize: Style.fontSizeXS * scaling color: { - if (buttonRoot.pending) return Color.mPrimary + if (buttonRoot.pending) + return Color.mPrimary + if (buttonRoot.isShutdown && !mouseArea.containsMouse) + return Color.mError + if (mouseArea.containsMouse) + return Color.mOnTertiary return Color.mOnSurfaceVariant } - opacity: 0.8 + opacity: Style.opacityHeavy wrapMode: Text.WordWrap } } @@ -319,29 +319,27 @@ NPanel { anchors.verticalCenter: parent.verticalCenter width: 24 * scaling height: 24 * scaling - radius: 12 * scaling + radius: width * 0.5 color: Color.mPrimary visible: buttonRoot.pending - + NText { anchors.centerIn: parent text: Math.ceil(timeRemaining / 1000) - font.pointSize: Style.fontSizeXS * scaling + font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightBold color: Color.mOnPrimary } } } - - MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - + onClicked: buttonRoot.clicked() } } -} \ No newline at end of file +} diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 0ad8dbc..b9b7df9 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -86,8 +86,6 @@ NBox { } } - - // ---------------------------------- // Uptime Timer { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 235fb50..80ba8f2 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -396,6 +396,25 @@ Singleton { } } + // Get current workspace + function getCurrentWorkspace() { + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i) + if (ws.isFocused) { + return ws + } + } + return null + } + + // Get focused window + function getFocusedWindow() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + return windows[focusedWindowIndex] + } + return null + } + // Generic logout/shutdown commands function logout() { if (isHyprland) { @@ -415,22 +434,15 @@ Singleton { } } - // Get current workspace - function getCurrentWorkspace() { - for (var i = 0; i < workspaces.count; i++) { - const ws = workspaces.get(i) - if (ws.isFocused) { - return ws - } - } - return null + function shutdown() { + Quickshell.execDetached(["shutdown", "-h", "now"]) } - // Get focused window - function getFocusedWindow() { - if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - return windows[focusedWindowIndex] - } - return null + function reboot() { + Quickshell.execDetached(["reboot"]) + } + + function suspend() { + Quickshell.execDetached(["systemctl", "suspend"]) } } From c6eb613e38a8a6819614ce693be651e022581cf7 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 19:35:41 -0400 Subject: [PATCH 33/35] IdleInhibitor: always use the coffee cup icon, the coloring takes care of the rest --- Modules/SidePanel/Cards/UtilitiesCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 1fe3e62..8d3b67a 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -36,7 +36,7 @@ NBox { // Idle Inhibitor NIconButton { - icon: IdleInhibitorService.isInhibited ? "coffee" : "bedtime" + icon: "coffee" tooltipText: IdleInhibitorService.isInhibited ? "Disable Keep Awake" : "Enable Keep Awake" colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary From caffbe45e0d599fb6ef5e3b133873efbf21c7539 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 20:14:49 -0400 Subject: [PATCH 34/35] Removed NLoader in favor of Loader, as it was not doing anything anymore --- Modules/Background/Background.qml | 72 +++++++++++++++------------- Modules/Background/Overview.qml | 2 +- Modules/Background/ScreenCorners.qml | 6 +-- Modules/Bar/Brightness.qml | 2 +- Modules/Bar/SidePanelToggle.qml | 23 +-------- Modules/Dock/Dock.qml | 6 +-- Modules/IPC/IPCManager.qml | 4 +- Modules/LockScreen/LockScreen.qml | 22 +++++---- Modules/PowerPanel/PowerPanel.qml | 4 +- Widgets/NLoader.qml | 39 --------------- 10 files changed, 65 insertions(+), 115 deletions(-) delete mode 100644 Widgets/NLoader.qml diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index ea8c721..54a7878 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -4,49 +4,53 @@ import Quickshell.Wayland import qs.Commons import qs.Services -Variants { - model: Quickshell.screens +Loader { + active: !Settings.data.wallpaper.swww.enabled - delegate: PanelWindow { - required property ShellScreen modelData - property string wallpaperSource: WallpaperService.currentWallpaper !== "" - && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + sourceComponent: Variants { + model: Quickshell.screens - visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled + delegate: PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" - // Force update when SWWW setting changes - onVisibleChanged: { - if (visible) { + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled - } else { + // Force update when SWWW setting changes + onVisibleChanged: { + if (visible) { + } else { + + } } - } - color: Color.transparent - screen: modelData - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-wallpaper" + color: Color.transparent + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-wallpaper" - anchors { - bottom: true - top: true - right: true - left: true - } + anchors { + bottom: true + top: true + right: true + left: true + } - margins { - top: 0 - } + margins { + top: 0 + } - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: wallpaperSource - visible: wallpaperSource !== "" - cache: true - smooth: true - mipmap: false + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: wallpaperSource + visible: wallpaperSource !== "" + cache: true + smooth: true + mipmap: false + } } } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 75a3a95..d45e95b 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -6,7 +6,7 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +Loader { active: CompositorService.isNiri Component.onCompleted: { diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index f8c612f..ce984a7 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -6,10 +6,10 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { - isLoaded: Settings.data.general.showScreenCorners +Loader { + active: Settings.data.general.showScreenCorners - content: Variants { + sourceComponent: Variants { model: Quickshell.screens PanelWindow { diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 0d26dfc..2388f42 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -70,7 +70,7 @@ Item { onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.Brightness - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } } diff --git a/Modules/Bar/SidePanelToggle.qml b/Modules/Bar/SidePanelToggle.qml index 542c5b2..42c634c 100644 --- a/Modules/Bar/SidePanelToggle.qml +++ b/Modules/Bar/SidePanelToggle.qml @@ -14,26 +14,5 @@ NIconButton { colorBorderHover: Color.transparent anchors.verticalCenter: parent.verticalCenter - onClicked: { - sidePanel.toggle(screen) - // sidePanel.isLoaded = !sidePanel.isLoaded - // Logger.log("SidePanelToggle", sidePanel.isLoaded) - // // Map this button's center to the screen and open the side panel below it - // const localCenterX = width / 2 - // const localCenterY = height / 2 - // const globalPoint = mapToItem(null, localCenterX, localCenterY) - // if (sidePanel.isLoaded) { - // // Call hide() instead of directly setting isLoaded to false - // if (sidePanel.item && sidePanel.item.hide) { - // sidePanel.item.hide() - // } else { - // sidePanel.isLoaded = false - // } - // } else if (sidePanel.openAt) { - // sidePanel.openAt(globalPoint.x, screen) - // } else { - // // Fallback: toggle if API unavailable - // sidePanel.isLoaded = true - // } - } + onClicked: sidePanel.toggle(screen) } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 3b9503e..641834e 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -9,9 +9,9 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { - isLoaded: (Settings.data.dock.monitors.length > 0) - content: Component { +Loader { + active: (Settings.data.dock.monitors.length > 0) + sourceComponent: Component { Variants { model: Quickshell.screens diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 83e350a..07a6351 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -47,8 +47,8 @@ Item { function toggle() { // Only lock if not already locked (prevents the red screen issue) // Note: No unlock via IPC for security reasons - if (!lockScreen.isLoaded) { - lockScreen.isLoaded = true + if (!lockScreen.active) { + lockScreen.active = true } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 57fd876..93413fb 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -12,12 +12,13 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +Loader { id: lockScreen + active: false // Log state changes to help debug lock screen issues - onIsLoadedChanged: { - Logger.log("LockScreen", "State changed - isLoaded:", isLoaded) + onActiveChanged: { + Logger.log("LockScreen", "State changed:", active) } // Allow a small grace period after unlocking so the compositor releases the lock surfaces @@ -26,23 +27,28 @@ NLoader { interval: 250 repeat: false onTriggered: { - Logger.log("LockScreen", "Unload timer triggered - setting isLoaded to false") - lockScreen.isLoaded = false + Logger.log("LockScreen", "Unload timer triggered - deactivating") + lockScreen.active = false } } function scheduleUnloadAfterUnlock() { Logger.log("LockScreen", "Scheduling unload after unlock") unloadAfterUnlockTimer.start() } - content: Component { + sourceComponent: Component { WlSessionLock { id: lock + // Tie session lock to loader visibility - locked: lockScreen.isLoaded + locked: lockScreen.active // Lockscreen is a different beast, needs a capital 'S' in 'Screen' to access the current screen // Also we use a different scaling algorithm based on the resolution, as the design is full screen - readonly property real scaling: ScalingService.dynamicScale(Screen) + readonly property real scaling: { + var tt = ScalingService.dynamicScale(Screen) + console.log(tt) + return tt + } property string errorMessage: "" property bool authenticating: false diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index d733186..a32211e 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -55,8 +55,8 @@ NPanel { switch (action) { case "lock": // Access lockScreen directly like IPCManager does - if (!lockScreen.isLoaded) { - lockScreen.isLoaded = true + if (!lockScreen.active) { + lockScreen.active = true } break case "suspend": diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml deleted file mode 100644 index a22ac03..0000000 --- a/Widgets/NLoader.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick - -// Example usage: -// NLoader { -// content: Component { -// YourComponent { -Loader { - id: loader - - // Boolean control to load/unload the item - property bool isLoaded: false - - // Provide the component to be loaded. - property Component content - - active: isLoaded - asynchronous: true - sourceComponent: content - - onActiveChanged: { - if (active && item && item.show) { - item.show() - } - } - - onItemChanged: { - if (active && item && item.show) { - item.show() - } - } - - Connections { - target: loader.item - ignoreUnknownSignals: true - function onDismissed() { - loader.isLoaded = false - } - } -} From e5c668c815d2e7a0693ae48baafc1c1b69c11fb8 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 20:35:36 -0400 Subject: [PATCH 35/35] Updated log message for overview --- Modules/Background/Overview.qml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index d45e95b..e673663 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -11,9 +11,7 @@ Loader { Component.onCompleted: { if (CompositorService.isNiri) { - Logger.log("Overview", "Loading Overview component (Niri detected)") - } else { - Logger.log("Overview", "Skipping Overview component (Niri not detected)") + Logger.log("Overview", "Loading Overview component for Niri") } }