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/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 diff --git a/Commons/Settings.qml b/Commons/Settings.qml index dd3507a..833426d 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 = [] @@ -82,16 +79,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 }) @@ -125,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 } @@ -216,13 +216,22 @@ Singleton { property bool showMiniplayerAlbumArt: false property bool showMiniplayerCava: false property string visualizerType: "linear" + property int volumeStep: 5 } // ui 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 + + // Idle inhibitor state + property bool idleInhibitorEnabled: false } // Scaling (not stored inside JsonObject, or it crashes) 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..6567430 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -12,388 +12,285 @@ import qs.Widgets import "../../Helpers/FuzzySort.js" as Fuzzysort -NLoader { - id: appLauncher - isLoaded: false - // Clipboard state is persisted in Services/ClipboardService.qml - content: Component { - NPanel { - id: appLauncherPanel +NPanel { + 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 { + id: calculator + } - // No local timer/processes; use persistent Clipboard service + ClipboardHistory { + id: clipboardHistory + } - // Removed local clipboard processes; handled by Clipboard service + // Properties + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 - // 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}'`]) + // 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 + } - function copyText(text) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + // 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() + } + } - function updateClipboardHistory() { - ClipboardService.refresh() + 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) } - - function selectNext() { - if (filteredEntries.length > 0) { - selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) - } + GradientStop { + position: 1.0 + color: Qt.darker(Color.mSurface, 1.1) } + } - function selectPrev() { - if (filteredEntries.length > 0) { - selectedIndex = Math.max(selectedIndex - 1, 0) - } - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling - function activateSelected() { - if (filteredEntries.length === 0) - return + // 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) - var modelData = filteredEntries[selectedIndex] - if (modelData && modelData.execute) { - if (modelData.isCommand) { - modelData.execute() - return - } else { - modelData.execute() + 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 } - appLauncherPanel.hide() - } - } - 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) + 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) } - }) - - 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 () {} + selectedTextColor: Color.mOnSurface + selectionColor: Color.mPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() }) } - } 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 () {} - }) + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: root.close() } - 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 - })) + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } } - Logger.log("AppLauncher", "Filtered entries:", results.length) - return results + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + } + } } - 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 - updateClipboardHistory() - } + // Applications list + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded - // 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 - - // 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 { + ListView { + id: appsList anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling + spacing: Style.marginXXS * scaling + model: filteredEntries + currentIndex: selectedIndex - // Search bar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.barHeight * scaling - Layout.bottomMargin: Style.marginM * scaling + 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 - selectedIndex = 0 // Reset selection when search changes - } - 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: appLauncherPanel.hide() + Behavior on color { + ColorAnimation { + duration: Style.animationFast } } @@ -408,181 +305,136 @@ NLoader { 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 - } - - Text { - 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() !== "" + } } } } diff --git a/Modules/AppLauncher/Calculator.qml b/Modules/AppLauncher/Calculator.qml new file mode 100644 index 0000000..8dae5bd --- /dev/null +++ b/Modules/AppLauncher/Calculator.qml @@ -0,0 +1,151 @@ +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..13e59c8 --- /dev/null +++ b/Modules/AppLauncher/ClipboardHistory.qml @@ -0,0 +1,157 @@ +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, + "execute"// Store the text data for the execute function + : 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/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..e673663 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -6,14 +6,12 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +Loader { active: CompositorService.isNiri 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") } } 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/Bar.qml b/Modules/Bar/Bar.qml index e2e4beb..6cc30a7 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 @@ -119,16 +122,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/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..eeb4c7f --- /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 + + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: 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 + } + } + } +} 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/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/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index 680a381..cd6dedb 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -32,8 +32,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 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/Bar/SidePanelToggle.qml b/Modules/Bar/SidePanelToggle.qml index 4fcb347..42c634c 100644 --- a/Modules/Bar/SidePanelToggle.qml +++ b/Modules/Bar/SidePanelToggle.qml @@ -14,23 +14,5 @@ NIconButton { colorBorderHover: Color.transparent 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 - } - } + onClicked: sidePanel.toggle(screen) } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 6718dd8..c6ba8c9 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,94 +122,37 @@ 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 - showOverlay: false // no colors overlay even if activated in settings + 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 + function open() { + visible = true - // Hide after animation completes - hideTimer.start() + // Register into the panel service + // so this will autoclose if we open another panel + PanelService.registerOpen(trayPanel) } - 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 } } } 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/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..7d28bd1 --- /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 + + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: 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/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index d879d90..762c8ff 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 + panelWidth: 340 * scaling + panelHeight: 320 * scaling + panelAnchorRight: 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/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/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 4c9a02c..07a6351 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -1,5 +1,7 @@ import QtQuick +import Quickshell import Quickshell.Io +import qs.Services Item { id: root @@ -8,7 +10,7 @@ Item { target: "settings" function toggle() { - settingsPanel.isLoaded = !settingsPanel.isLoaded + settingsPanel.toggle(Quickshell.screens[0]) } } @@ -16,7 +18,7 @@ Item { target: "notifications" function toggleHistory() { - notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded + notificationHistoryPanel.toggle(Quickshell.screens[0]) } function toggleDoNotDisturb() {// TODO @@ -26,7 +28,8 @@ Item { IpcHandler { target: "idleInhibitor" - function toggle() {// TODO + function toggle() { + return IdleInhibitorService.manualToggle() } } @@ -34,7 +37,7 @@ Item { target: "appLauncher" function toggle() { - appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded + appLauncherPanel.toggle(Quickshell.screens[0]) } } @@ -42,7 +45,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.active) { + lockScreen.active = true + } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index aa1facf..93413fb 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -12,27 +12,43 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +Loader { id: lockScreen + active: false + + // Log state changes to help debug lock screen issues + onActiveChanged: { + Logger.log("LockScreen", "State changed:", active) + } + // 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 - 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 @@ -233,13 +249,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 +277,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 +420,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 +440,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 +466,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 +504,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 +525,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 +551,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 +601,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 +634,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/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) + } }) } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index a52950e..6c7e465 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -8,267 +8,180 @@ 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 + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: 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() - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling - Connections { - target: notificationPanel - ignoreUnknownSignals: true - function onDismissed() { - // Start hide animation - notificationRect.scaleValue = 0.8 - notificationRect.opacityValue = 0.0 + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling - // Hide after animation completes - hideTimer.start() - } - } - - // 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 } + } - 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/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml new file mode 100644 index 0000000..a32211e --- /dev/null +++ b/Modules/PowerPanel/PowerPanel.qml @@ -0,0 +1,345 @@ +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: 440 * scaling + panelHeight: 380 * scaling + panelAnchorCentered: true + + // Timer properties + property int timerDuration: 9000 // 9 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.active) { + lockScreen.active = 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() + } + + // 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)} seconds...` : "Power Options" + font.weight: Style.fontWeightBold + font.pointSize: Style.fontSizeL * scaling + color: timerActive ? Color.mPrimary : Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: timerActive ? "back_hand" : "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" + 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.6 * scaling + radius: Style.radiusS * scaling + color: { + if (pending) + return Color.applyOpacity(Color.mPrimary, "20") + if (mouseArea.containsMouse) + return Color.mTertiary + return Color.transparent + } + + border.width: pending ? Math.max(Style.borderM * scaling) : 0 + border.color: pending ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Item { + anchors.fill: parent + anchors.margins: Style.marginL * 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.mOnTertiary + return Color.mOnSurface + } + font.pointSize: Style.fontSizeXXXL * scaling + width: Style.baseWidgetSize * 0.6 * scaling + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // 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.marginXL * scaling + anchors.rightMargin: pendingIndicator.visible ? Style.marginM * scaling : 0 + spacing: 0 + + NText { + text: buttonRoot.title + font.weight: Style.fontWeightMedium + 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.mOnTertiary + return Color.mOnSurface + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + 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 + if (buttonRoot.isShutdown && !mouseArea.containsMouse) + return Color.mError + if (mouseArea.containsMouse) + return Color.mOnTertiary + return Color.mOnSurfaceVariant + } + opacity: Style.opacityHeavy + 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: width * 0.5 + color: Color.mPrimary + visible: buttonRoot.pending + + NText { + anchors.centerIn: parent + text: Math.ceil(timeRemaining / 1000) + 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() + } + } +} diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 7012838..43fb19c 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 + 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 { 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/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index cbab92b..e10af61 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/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index ae5f76f..8da9077 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -157,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 + } + } + } } } } 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..b9b7df9 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) } } @@ -70,18 +70,22 @@ NBox { icon: "power_settings_new" tooltipText: "Power Menu" onClicked: { - powerMenu.show() + powerPanel.open(screen) + sidePanel.close() + } + } + + NIconButton { + id: closeButton + icon: "close" + tooltipText: "Close Side Panel" + onClicked: { + sidePanel.close() } } } } - PowerMenu { - id: powerMenu - anchors.top: powerButton.bottom - anchors.right: powerButton.right - } - // ---------------------------------- // Uptime Timer { diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9bc0739..8d3b67a 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 } @@ -31,13 +34,24 @@ NBox { } } + // Idle Inhibitor + NIconButton { + 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 + onClicked: { + IdleInhibitorService.manualToggle() + } + } + // Wallpaper NIconButton { icon: "image" tooltipText: "Open Wallpaper Selector" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } diff --git a/Modules/SidePanel/PowerMenu.qml b/Modules/SidePanel/PowerMenu.qml deleted file mode 100644 index 84ee679..0000000 --- a/Modules/SidePanel/PowerMenu.qml +++ /dev/null @@ -1,376 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import qs.Commons -import qs.Services -import qs.Widgets -import qs.Modules.LockScreen - -NPanel { - id: powerMenu - visible: 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 - 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 - - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: Style.marginL * scaling - anchors.topMargin: 86 * scaling - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - onClicked: { - - } - } - - 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 - onClicked: { - Logger.log("PowerMenu", "Lock screen requested") - // Lock the screen - lockScreen.isLoaded = true - powerMenu.visible = false - } - } - } - - // -------------- - // 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 - onClicked: { - suspend() - powerMenu.visible = false - } - } - } - - // -------------- - // 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 - onClicked: { - reboot() - powerMenu.visible = false - } - } - } - - // -------------- - // 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 - onClicked: { - logout() - powerMenu.visible = false - } - } - } - - // -------------- - // 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 - onClicked: { - shutdown() - powerMenu.visible = false - } - } - } - } - } - - // ---------------------------------- - // 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 - } -} diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 04bfa3f..bfbff83 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -7,205 +7,76 @@ 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 + panelWidth: 460 * scaling + panelHeight: 700 * scaling + panelAnchorRight: 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 - } - } + 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/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/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 } diff --git a/Services/CavaService.qml b/Services/CavaService.qml index e821009..3600d62 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 || Settings.data.audio.showMiniplayerCava) + running: (Settings.data.audio.visualizerType !== "none") && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava) command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml index 008e83c..899ec77 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,32 @@ 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 +133,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 +153,76 @@ 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() + } } 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"]) } } diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml new file mode 100644 index 0000000..3a9aedb --- /dev/null +++ b/Services/IdleInhibitorService.qml @@ -0,0 +1,183 @@ +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") + } + } + + // 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/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/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/NLoader.qml b/Widgets/NLoader.qml deleted file mode 100644 index 322499a..0000000 --- a/Widgets/NLoader.qml +++ /dev/null @@ -1,42 +0,0 @@ -import QtQuick - -// Example usage: -// NLoader { -// content: Component { -// NPanel { -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 - - // onLoaded: { - // Logger.log("NLoader", "OnLoaded:", item.toString()); - // } - 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 - } - } -} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8b8efe5..9dd2aa4 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -4,111 +4,179 @@ 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 panelWidth: 1500 + property int panelHeight: 400 + property bool panelAnchorCentered: false + property bool panelAnchorLeft: false + property bool panelAnchorRight: 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" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + + 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: panelWidth + height: panelHeight + + anchors { + 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: !panelAnchorCentered + && (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined + bottomMargin: !panelAnchorCentered + && (Settings.data.bar.position === "bottom") ? Style.marginS * scaling : undefined + rightMargin: !panelAnchorCentered && panelAnchorRight ? 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 + } + } } } } diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml new file mode 100644 index 0000000..77d1812 --- /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 !== "" + } + } + + // 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 + 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 + } + } + } + } +} 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 diff --git a/shell.qml b/shell.qml index de85aee..6bfe060 100644 --- a/shell.qml +++ b/shell.qml @@ -17,14 +17,15 @@ 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 import qs.Modules.Notification import qs.Modules.SettingsPanel +import qs.Modules.PowerPanel import qs.Modules.SidePanel import qs.Modules.Toast + import qs.Services import qs.Widgets @@ -41,10 +42,6 @@ ShellRoot { id: appLauncherPanel } - DemoPanel { - id: demoPanel - } - SidePanel { id: sidePanel } @@ -69,6 +66,10 @@ ShellRoot { id: lockScreen } + PowerPanel { + id: powerPanel + } + ToastManager {} IPCManager {}