Merge branch 'main' into shebang-fixes

This commit is contained in:
Lemmy 2025-08-20 20:43:09 -04:00 committed by GitHub
commit 1bd7ec27e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 3616 additions and 3550 deletions

View file

@ -5,7 +5,7 @@
"mSecondary": "#9ccfd8", "mSecondary": "#9ccfd8",
"mOnSecondary": "#191724", "mOnSecondary": "#191724",
"mTertiary": "#31748f", "mTertiary": "#31748f",
"mOnTertiary": "#e0def4", "mOnTertiary": "#191724",
"mError": "#eb6f92", "mError": "#eb6f92",
"mOnError": "#1f1d2e", "mOnError": "#1f1d2e",
"mSurface": "#191724", "mSurface": "#191724",

View file

@ -1,7 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env -S bash
# A Bash script to monitor system stats and output them in JSON format. # A Bash script to monitor system stats and output them in JSON format.
# This script is a conversion of ZigStat
# --- Configuration --- # --- Configuration ---
# Default sleep duration in seconds. Can be overridden by the first argument. # Default sleep duration in seconds. Can be overridden by the first argument.
@ -114,7 +113,7 @@ get_cpu_temp() {
local name local name
name=$(<"$dir/name") name=$(<"$dir/name")
# Check for supported sensor types. # 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_PATH=$dir
TEMP_SENSOR_TYPE=$name TEMP_SENSOR_TYPE=$name
break # Found it, no need to keep searching. break # Found it, no need to keep searching.
@ -172,6 +171,24 @@ get_cpu_temp() {
else else
echo 0 # Fallback echo 0 # Fallback
fi 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 else
echo 0 # Should not happen if cache logic is correct. echo 0 # Should not happen if cache logic is correct.
fi fi

View file

@ -30,9 +30,6 @@ Singleton {
// Flag to prevent unnecessary wallpaper calls during reloads // Flag to prevent unnecessary wallpaper calls during reloads
property bool isInitialLoad: true 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 to validate monitor configurations
function validateMonitorConfigurations() { function validateMonitorConfigurations() {
var availableScreenNames = [] var availableScreenNames = []
@ -82,16 +79,19 @@ Singleton {
reload() reload()
} }
onLoaded: function () { onLoaded: function () {
Logger.log("Settings", "OnLoaded")
Qt.callLater(function () { Qt.callLater(function () {
// Only set wallpaper on initial load, not on reloads if (isInitialLoad) {
if (isInitialLoad && adapter.wallpaper.current !== "") { Logger.log("Settings", "OnLoaded")
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) // Only set wallpaper on initial load, not on reloads
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) 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 // Validate monitor configurations, only once
validateMonitorConfigurations() // if none of the configured monitors exist, clear the lists
validateMonitorConfigurations()
}
isInitialLoad = false isInitialLoad = false
}) })
@ -125,7 +125,7 @@ Singleton {
general: JsonObject { general: JsonObject {
property string avatarImage: defaultAvatar property string avatarImage: defaultAvatar
property bool dimDesktop: true property bool dimDesktop: false
property bool showScreenCorners: false property bool showScreenCorners: false
property real radiusRatio: 1.0 property real radiusRatio: 1.0
} }
@ -213,14 +213,25 @@ Singleton {
property JsonObject audio property JsonObject audio
audio: JsonObject { audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear" property string visualizerType: "linear"
property int volumeStep: 5
} }
// ui // ui
property JsonObject ui property JsonObject ui
ui: JsonObject { 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) // Scaling (not stored inside JsonObject, or it crashes)

152
Helpers/AdvancedMath.js Normal file
View file

@ -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"
];
}

View file

@ -12,388 +12,285 @@ import qs.Widgets
import "../../Helpers/FuzzySort.js" as Fuzzysort import "../../Helpers/FuzzySort.js" as Fuzzysort
NLoader { NPanel {
id: appLauncher id: root
isLoaded: false panelWidth: Math.min(700 * scaling, screen?.width * 0.75)
// Clipboard state is persisted in Services/ClipboardService.qml panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
content: Component { panelAnchorCentered: true
NPanel {
id: appLauncherPanel
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 // Refresh clipboard when user starts typing clipboard commands
function copyImageBase64(mime, base64) { onSearchTextChanged: {
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) 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) { // Regular app search
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) 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() { Component.onCompleted: {
ClipboardService.refresh() Logger.log("AppLauncher", "Component completed")
Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined')
if (typeof DesktopEntries !== 'undefined') {
Logger.log("AppLauncher", "DesktopEntries.entries:",
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
}
// Start clipboard refresh immediately on open
clipboardHistory.refresh()
}
// Main content container
panelContent: Rectangle {
// Subtle gradient background
gradient: Gradient {
GradientStop {
position: 0.0
color: Qt.lighter(Color.mSurface, 1.02)
} }
GradientStop {
function selectNext() { position: 1.0
if (filteredEntries.length > 0) { color: Qt.darker(Color.mSurface, 1.1)
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
}
} }
}
function selectPrev() { ColumnLayout {
if (filteredEntries.length > 0) { anchors.fill: parent
selectedIndex = Math.max(selectedIndex - 1, 0) anchors.margins: Style.marginL * scaling
} spacing: Style.marginM * scaling
}
function activateSelected() { // Search bar
if (filteredEntries.length === 0) Rectangle {
return 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] Item {
if (modelData && modelData.execute) { anchors.fill: parent
if (modelData.isCommand) { anchors.margins: Style.marginM * scaling
modelData.execute()
return NIcon {
} else { id: searchIcon
modelData.execute() 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 TextField {
property string searchText: "" id: searchInput
property int selectedIndex: 0 placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..."
color: Color.mOnSurface
// Refresh clipboard when user starts typing clipboard commands placeholderTextColor: Color.mOnSurfaceVariant
onSearchTextChanged: { background: null
if (searchText.startsWith(">clip")) { font.pointSize: Style.fontSizeL * scaling
ClipboardService.refresh() anchors.left: searchIcon.right
} anchors.leftMargin: Style.marginS * scaling
} anchors.right: parent.right
property var filteredEntries: { anchors.verticalCenter: parent.verticalCenter
Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) onTextChanged: {
if (!desktopEntries || desktopEntries.length === 0) { searchText = text
Logger.log("AppLauncher", "No desktop entries available") // Defer selectedIndex reset to avoid binding loops
return [] Qt.callLater(() => selectedIndex = 0)
}
// 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)
} }
}) selectedTextColor: Color.mOnSurface
selectionColor: Color.mPrimary
if (results.length === 0) { padding: 0
results.push({ verticalAlignment: TextInput.AlignVCenter
"isClipboard": true, leftPadding: 0
"name": "No clipboard history", rightPadding: 0
"content": "No matching clipboard entries found", topPadding: 0
"icon": "content_paste_off" bottomPadding: 0
}) font.bold: true
} Component.onCompleted: {
// Focus the search bar by default
return results Qt.callLater(() => {
} searchInput.forceActiveFocus()
// Handle direct math expressions after ">"
if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
var mathExpr = query.slice(1).trim()
// Check if it looks like a math expression (contains numbers and math operators)
if (mathExpr && /[0-9+\-*/().]/.test(mathExpr)) {
try {
var sanitizedExpr = mathExpr.replace(/[^0-9+\-*/().\s]/g, '')
var result = eval(sanitizedExpr)
if (isFinite(result) && !isNaN(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '')
results.push({
"isCalculator": true,
"name": `${mathExpr} = ${displayResult}`,
"result": result,
"expr": mathExpr,
"icon": "tag",
"execute": function () {
Quickshell.clipboardText = displayResult
copyText(displayResult)
Quickshell.execDetached(
["notify-send", "Calculator", `${mathExpr} = ${displayResult} (copied to clipboard)`])
}
})
return results
}
} catch (error) {
// If math evaluation fails, fall through to regular search
}
}
}
// Handle calculator
if (query.startsWith(">calc")) {
var expr = searchText.slice(5).trim()
if (expr && expr !== "") {
try {
// Simple evaluation - only allow basic math operations
var sanitizedExpr = expr.replace(/[^0-9+\-*/().\s]/g, '')
var result = eval(sanitizedExpr)
if (isFinite(result) && !isNaN(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/,
'')
results.push({
"isCalculator": true,
"name": `${expr} = ${displayResult}`,
"result": result,
"expr": expr,
"icon": "tag",
"execute": function () {
Quickshell.clipboardText = displayResult
copyText(displayResult)
Quickshell.execDetached(
["notify-send", "Calculator", `${expr} = ${displayResult} (copied to clipboard)`])
}
})
} else {
results.push({
"isCalculator": true,
"name": "Invalid expression",
"content": "Please enter a valid mathematical expression",
"icon": "tag",
"execute": function () {}
})
}
} catch (error) {
results.push({
"isCalculator": true,
"name": "Invalid expression",
"content": "Please enter a valid mathematical expression",
"icon": "tag",
"execute": function () {}
}) })
} }
} else { Keys.onDownPressed: selectNext()
// Show placeholder when just ">calc" is entered Keys.onUpPressed: selectPrev()
results.push({ Keys.onEnterPressed: activateSelected()
"isCalculator": true, Keys.onReturnPressed: activateSelected()
"name": "Calculator", Keys.onEscapePressed: root.close()
"content": "Enter a mathematical expression (e.g., 5+5, 2*3, 10/2)",
"icon": "tag",
"execute": function () {}
})
} }
return results
} }
// Regular app search Behavior on border.color {
if (!query) { ColorAnimation {
results = results.concat(visibleEntries.sort(function (a, b) { duration: Style.animationFast
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) Behavior on border.width {
return results NumberAnimation {
duration: Style.animationFast
}
}
} }
Component.onCompleted: { // Applications list
Logger.log("AppLauncher", "Component completed") ScrollView {
Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') Layout.fillWidth: true
if (typeof DesktopEntries !== 'undefined') { Layout.fillHeight: true
Logger.log("AppLauncher", "DesktopEntries.entries:", clip: true
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
} ScrollBar.vertical.policy: ScrollBar.AsNeeded
// Start clipboard refresh immediately on open
updateClipboardHistory()
}
// Main content container ListView {
Rectangle { id: appsList
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 {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL * scaling spacing: Style.marginXXS * scaling
spacing: Style.marginM * scaling model: filteredEntries
currentIndex: selectedIndex
// Search bar delegate: Rectangle {
Rectangle { width: appsList.width - Style.marginS * scaling
Layout.fillWidth: true height: 65 * scaling
Layout.preferredHeight: Style.barHeight * scaling
Layout.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurface property bool isSelected: index === selectedIndex
border.color: searchInput.activeFocus ? Color.mPrimary : Color.mOutline color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurface
border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling) border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent
border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderM * scaling : 0)
Item { Behavior on color {
anchors.fill: parent ColorAnimation {
anchors.margins: Style.marginM * scaling duration: Style.animationFast
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()
} }
} }
@ -408,181 +305,136 @@ NLoader {
duration: Style.animationFast duration: Style.animationFast
} }
} }
}
// Applications list RowLayout {
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ListView {
id: appsList
anchors.fill: parent anchors.fill: parent
spacing: Style.marginXXS * scaling anchors.margins: Style.marginM * scaling
model: filteredEntries spacing: Style.marginM * scaling
currentIndex: selectedIndex
delegate: Rectangle { // App icon with background
width: appsList.width - Style.marginS * scaling Rectangle {
height: 65 * scaling Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling
radius: Style.radiusM * scaling Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling
property bool isSelected: index === selectedIndex radius: Style.radiusS * scaling
color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurface color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurfaceVariant
border.color: (appCardArea.containsMouse || isSelected) ? Color.mPrimary : Color.transparent property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
border.width: Math.max(1, (appCardArea.containsMouse || isSelected) ? Style.borderM * scaling : 0) || (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 { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Style.animationFast duration: Style.animationFast
} }
} }
}
Behavior on border.color { // App info
ColorAnimation { ColumnLayout {
duration: Style.animationFast 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 { NText {
NumberAnimation { text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "")
duration: Style.animationFast font.pointSize: Style.fontSizeM * scaling
} color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface
} elide: Text.ElideRight
Layout.fillWidth: true
RowLayout { visible: text !== ""
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()
}
} }
} }
} }
}
// No results message MouseArea {
NText { id: appCardArea
text: searchText.trim() !== "" ? "No applications found" : "No applications available" anchors.fill: parent
font.pointSize: Style.fontSizeL * scaling hoverEnabled: true
color: Color.mOnSurface cursorShape: Qt.PointingHandCursor
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: filteredEntries.length === 0
}
// Results count onClicked: {
NText { selectedIndex = index
text: searchText.startsWith( activateSelected()
">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() !== ""
} }
} }
} }
// 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() !== ""
}
} }
} }
} }

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -4,49 +4,53 @@ import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
Variants { Loader {
model: Quickshell.screens active: !Settings.data.wallpaper.swww.enabled
delegate: PanelWindow { sourceComponent: Variants {
required property ShellScreen modelData model: Quickshell.screens
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
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 visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
onVisibleChanged: {
if (visible) {
} else { // Force update when SWWW setting changes
onVisibleChanged: {
if (visible) {
} else {
}
} }
} color: Color.transparent
color: Color.transparent screen: modelData
screen: modelData WlrLayershell.layer: WlrLayer.Background
WlrLayershell.layer: WlrLayer.Background WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell-wallpaper"
WlrLayershell.namespace: "quickshell-wallpaper"
anchors { anchors {
bottom: true bottom: true
top: true top: true
right: true right: true
left: true left: true
} }
margins { margins {
top: 0 top: 0
} }
Image { Image {
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: wallpaperSource source: wallpaperSource
visible: wallpaperSource !== "" visible: wallpaperSource !== ""
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false
}
} }
} }
} }

View file

@ -6,14 +6,12 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
active: CompositorService.isNiri active: CompositorService.isNiri
Component.onCompleted: { Component.onCompleted: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
Logger.log("Overview", "Loading Overview component (Niri detected)") Logger.log("Overview", "Loading Overview component for Niri")
} else {
Logger.log("Overview", "Skipping Overview component (Niri not detected)")
} }
} }

View file

@ -6,10 +6,10 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
isLoaded: Settings.data.general.showScreenCorners active: Settings.data.general.showScreenCorners
content: Variants { sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
PanelWindow { PanelWindow {

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -17,6 +18,8 @@ Variants {
readonly property real scaling: ScalingService.scale(screen) readonly property real scaling: ScalingService.scale(screen)
screen: modelData screen: modelData
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Style.barHeight * scaling implicitHeight: Style.barHeight * scaling
color: Color.transparent color: Color.transparent
@ -119,16 +122,6 @@ Variants {
anchors.verticalCenter: parent.verticalCenter 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 {} SidePanelToggle {}
} }
} }

View file

@ -31,25 +31,10 @@ NIconButton {
} }
tooltipText: "Bluetooth Devices" tooltipText: "Bluetooth Devices"
onClicked: { onClicked: {
if (!bluetoothMenuLoader.active) { bluetoothPanel.toggle(screen)
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
}
}
} }
BluetoothMenu { BluetoothPanel {
id: bluetoothMenuLoader id: bluetoothPanel
} }
} }

View file

@ -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
}
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -70,7 +70,7 @@ Item {
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }
} }

View file

@ -24,7 +24,7 @@ Rectangle {
} }
onEntered: { onEntered: {
if (!calendarPanel.isLoaded) { if (!calendarPanel.active) {
tooltip.show() tooltip.show()
} }
} }
@ -33,7 +33,7 @@ Rectangle {
} }
onClicked: { onClicked: {
tooltip.hide() tooltip.hide()
calendarPanel.isLoaded = !calendarPanel.isLoaded calendarPanel.toggle(screen)
} }
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Modules.Audio
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -31,8 +32,6 @@ Row {
height: Math.round(Style.capsuleHeight * scaling) height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Math.round(Style.borderS * scaling))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -42,19 +41,102 @@ Row {
anchors.leftMargin: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: 20 * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
}
}
Row { Row {
id: row id: row
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
z: 1 // Above the visualizer
// Window icon
NIcon { NIcon {
id: windowIcon id: windowIcon
text: MediaService.isPlaying ? "pause" : "play_arrow" text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
}
Column {
anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
Rectangle {
width: 16 * scaling
height: 16 * scaling
radius: width * 0.5
color: Color.transparent
antialiasing: true
clip: true
NImageRounded {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
anchors.margins: scaling
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
borderWidth: 0
border.color: Color.transparent
imageRadius: width
antialiasing: true
}
// Fallback icon when no album art available
NIcon {
id: windowIconFallback
text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && !trackArt.visible
}
}
} }
NText { NText {

View file

@ -20,21 +20,6 @@ NIconButton {
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
onClicked: { onClicked: {
if (!notificationHistoryPanel.active) { notificationHistoryPanel.toggle(screen)
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
}
}
} }
} }

View file

@ -14,23 +14,5 @@ NIconButton {
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: { onClicked: sidePanel.toggle(screen)
// 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
}
}
} }

View file

@ -76,37 +76,34 @@ Rectangle {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { trayPanel.close()
trayMenu.hideMenu()
}
if (!modelData.onlyMenu) { if (!modelData.onlyMenu) {
modelData.activate() modelData.activate()
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { trayPanel.close()
trayMenu.hideMenu()
}
modelData.secondaryActivate && modelData.secondaryActivate() modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
trayTooltip.hide() trayTooltip.hide()
// If menu is already visible, close it
if (trayMenu && trayMenu.visible) { // Close the menu if it was visible
trayMenu.hideMenu() if (trayPanel && trayPanel.visible) {
trayPanel.close()
return return
} }
if (modelData.hasMenu && modelData.menu && trayMenu) { if (modelData.hasMenu && modelData.menu && trayMenu) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon // Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2) const menuX = (width / 2) - (trayMenu.width / 2)
const menuY = (Style.barHeight * scaling) const menuY = (Style.barHeight * scaling)
trayMenu.menu = modelData.menu trayMenu.menu = modelData.menu
trayMenu.showAt(parent, menuX, menuY) trayMenu.showAt(parent, menuX, menuY)
trayPanel.show()
} else { } else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
} }
} }
@ -125,94 +122,37 @@ Rectangle {
} }
} }
// Attached TrayMenu drop down PanelWindow {
// Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu
NPanel {
id: trayPanel 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 open() {
function hide() { visible = true
// Start hide animation
trayMenuRect.scaleValue = 0.8
trayMenuRect.opacityValue = 0.0
// Hide after animation completes // Register into the panel service
hideTimer.start() // so this will autoclose if we open another panel
PanelService.registerOpen(trayPanel)
} }
Connections { function close() {
target: trayPanel visible = false
ignoreUnknownSignals: true trayMenu.hideMenu()
function onDismissed() {
// Start hide animation
trayMenuRect.scaleValue = 0.8
trayMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
} }
// Also handle visibility changes from external sources // Clicking outside of the rectangle to close
onVisibleChanged: { MouseArea {
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
anchors.fill: parent anchors.fill: parent
onClicked: trayPanel.close()
}
// Animation properties TrayMenu {
property real scaleValue: 0.8 id: trayMenu
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
}
} }
} }
} }

View file

@ -65,7 +65,7 @@ Item {
} }
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }
} }

View file

@ -34,27 +34,10 @@ NIconButton {
} }
tooltipText: "WiFi Networks" tooltipText: "WiFi Networks"
onClicked: { onClicked: {
if (!wifiMenuLoader.active) { wifiPanel.toggle(screen)
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()
}
}
} }
WiFiMenu { WiFiPanel {
id: wifiMenuLoader id: wifiPanel
} }
} }

View file

@ -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
}
}
}
}
}
}
}
}
}
}
}
}
}

335
Modules/Bar/WiFiPanel.qml Normal file
View file

@ -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
}
}
}
}
}
}
}
}
}
}
}

View file

@ -7,227 +7,132 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NPanel {
id: root id: root
content: Component { panelWidth: 340 * scaling
NPanel { panelHeight: 320 * scaling
id: calendarPanel panelAnchorRight: true
// Override hide function to animate first // Main Column
function hide() { panelContent: ColumnLayout {
// Start hide animation anchors.fill: parent
calendarRect.scaleValue = 0.8 anchors.margins: Style.marginM * scaling
calendarRect.opacityValue = 0.0 spacing: Style.marginXS * scaling
// Hide after animation completes // Header: Month/Year with navigation
hideTimer.start() 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 { Connections {
target: calendarPanel target: calendarPanel
function onDismissed() { function onVisibleChanged() {
// Start hide animation if (calendarPanel.visible) {
calendarRect.scaleValue = 0.8 grid.month = Time.date.getMonth()
calendarRect.opacityValue = 0.0 grid.year = Time.date.getFullYear()
}
// Hide after animation completes
hideTimer.start()
} }
} }
// Also handle visibility changes from external sources delegate: Rectangle {
onVisibleChanged: { width: (Style.baseWidgetSize * scaling)
if (!visible && calendarRect.opacityValue > 0) { height: (Style.baseWidgetSize * scaling)
// Start hide animation radius: Style.radiusS * scaling
calendarRect.scaleValue = 0.8 color: model.today ? Color.mPrimary : Color.transparent
calendarRect.opacityValue = 0.0
// Hide after animation completes NText {
hideTimer.start() anchors.centerIn: parent
} text: model.day
} color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
// Timer to hide panel after animation font.pointSize: (Style.fontSizeM * scaling)
Timer { font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
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
} }
// Prevent closing when clicking in the panel bg Behavior on color {
MouseArea { ColorAnimation {
anchors.fill: parent duration: Style.animationFast
}
// 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
}
}
}
} }
} }
} }

View file

@ -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
}
}
}
}
}
}
}
}

View file

@ -9,9 +9,9 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
isLoaded: (Settings.data.dock.monitors.length > 0) active: (Settings.data.dock.monitors.length > 0)
content: Component { sourceComponent: Component {
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens

View file

@ -1,5 +1,7 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Item { Item {
id: root id: root
@ -8,7 +10,7 @@ Item {
target: "settings" target: "settings"
function toggle() { function toggle() {
settingsPanel.isLoaded = !settingsPanel.isLoaded settingsPanel.toggle(Quickshell.screens[0])
} }
} }
@ -16,7 +18,7 @@ Item {
target: "notifications" target: "notifications"
function toggleHistory() { function toggleHistory() {
notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded notificationHistoryPanel.toggle(Quickshell.screens[0])
} }
function toggleDoNotDisturb() {// TODO function toggleDoNotDisturb() {// TODO
@ -26,7 +28,8 @@ Item {
IpcHandler { IpcHandler {
target: "idleInhibitor" target: "idleInhibitor"
function toggle() {// TODO function toggle() {
return IdleInhibitorService.manualToggle()
} }
} }
@ -34,7 +37,7 @@ Item {
target: "appLauncher" target: "appLauncher"
function toggle() { function toggle() {
appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded appLauncherPanel.toggle(Quickshell.screens[0])
} }
} }
@ -42,7 +45,11 @@ Item {
target: "lockScreen" target: "lockScreen"
function toggle() { 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
}
} }
} }

View file

@ -12,27 +12,43 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
id: lockScreen 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 // Allow a small grace period after unlocking so the compositor releases the lock surfaces
Timer { Timer {
id: unloadAfterUnlockTimer id: unloadAfterUnlockTimer
interval: 250 interval: 250
repeat: false repeat: false
onTriggered: lockScreen.isLoaded = false onTriggered: {
Logger.log("LockScreen", "Unload timer triggered - deactivating")
lockScreen.active = false
}
} }
function scheduleUnloadAfterUnlock() { function scheduleUnloadAfterUnlock() {
Logger.log("LockScreen", "Scheduling unload after unlock")
unloadAfterUnlockTimer.start() unloadAfterUnlockTimer.start()
} }
content: Component { sourceComponent: Component {
WlSessionLock { WlSessionLock {
id: lock id: lock
// Tie session lock to loader visibility // 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 // 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 // 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 string errorMessage: ""
property bool authenticating: false property bool authenticating: false
@ -233,13 +249,13 @@ NLoader {
// Time display - Large and prominent with pulse animation // Time display - Large and prominent with pulse animation
Column { Column {
spacing: Style.marginS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Text { NText {
id: timeText id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm") text: Qt.formatDateTime(new Date(), "HH:mm")
font.family: "Inter" font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXXL * 6 * scaling font.pointSize: Style.fontSizeXXXL * 6 * scaling
font.weight: Font.Bold font.weight: Font.Bold
font.letterSpacing: -2 * scaling font.letterSpacing: -2 * scaling
@ -261,10 +277,10 @@ NLoader {
} }
} }
Text { NText {
id: dateText id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d") text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.family: "Inter" font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
font.weight: Font.Light font.weight: Font.Light
color: Color.mOnSurface color: Color.mOnSurface
@ -404,10 +420,10 @@ NLoader {
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "SECURE TERMINAL" text: "SECURE TERMINAL"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
Layout.fillWidth: true Layout.fillWidth: true
@ -424,10 +440,10 @@ NLoader {
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
} }
Text { NText {
text: Math.round(batteryIndicator.percent) + "%" text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
@ -450,19 +466,19 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "root@noctalia:~$" text: "root@noctalia:~$"
color: Color.mPrimary color: Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
Text { NText {
id: welcomeText id: welcomeText
text: "" text: ""
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0 property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
@ -488,18 +504,18 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "root@noctalia:~$" text: "root@noctalia:~$"
color: Color.mPrimary color: Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
Text { NText {
text: "sudo unlock-session" text: "sudo unlock-session"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
} }
@ -509,7 +525,7 @@ NLoader {
width: 0 width: 0
height: 0 height: 0
visible: false visible: false
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface color: Color.mOnSurface
echoMode: TextInput.Password echoMode: TextInput.Password
@ -535,11 +551,11 @@ NLoader {
} }
// Visual password display with integrated cursor // Visual password display with integrated cursor
Text { NText {
id: asterisksText id: asterisksText
text: "*".repeat(passwordInput.text.length) text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus visible: passwordInput.activeFocus
@ -585,7 +601,7 @@ NLoader {
} }
// Status messages // Status messages
Text { NText {
text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "")
color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent)
font.family: "DejaVu Sans Mono" font.family: "DejaVu Sans Mono"
@ -618,11 +634,11 @@ NLoader {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -12 * scaling Layout.bottomMargin: -12 * scaling
Text { NText {
anchors.centerIn: parent anchors.centerIn: parent
text: lock.authenticating ? "EXECUTING" : "EXECUTE" text: lock.authenticating ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }

View file

@ -47,13 +47,29 @@ Variants {
// Connect to animation signal from service // Connect to animation signal from service
Component.onCompleted: { Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notification, index) { NotificationService.animateAndRemove.connect(function (notification, index) {
// Find the delegate and trigger its animation // Prefer lookup by identity to avoid index mismatches
if (notificationStack.children && notificationStack.children[index]) { var delegate = null
let delegate = notificationStack.children[index] if (notificationStack.children && notificationStack.children.length > 0) {
if (delegate && delegate.animateOut) { for (var i = 0; i < notificationStack.children.length; i++) {
delegate.animateOut() 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)
}
}) })
} }

View file

@ -8,267 +8,180 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
// Loader for Notification History panel // Notification History panel
NLoader { NPanel {
id: root id: root
content: Component { panelWidth: 380 * scaling
NPanel { panelHeight: 500 * scaling
id: notificationPanel panelAnchorRight: true
// Override hide function to animate first panelContent: Rectangle {
function hide() { id: notificationRect
// Start hide animation color: Color.transparent
notificationRect.scaleValue = 0.8
notificationRect.opacityValue = 0.0
// Hide after animation completes ColumnLayout {
hideTimer.start() anchors.fill: parent
} anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
Connections { RowLayout {
target: notificationPanel Layout.fillWidth: true
ignoreUnknownSignals: true spacing: Style.marginM * scaling
function onDismissed() {
// Start hide animation
notificationRect.scaleValue = 0.8
notificationRect.opacityValue = 0.0
// Hide after animation completes NIcon {
hideTimer.start() text: "notifications"
} font.pointSize: Style.fontSizeXXL * scaling
} color: Color.mPrimary
// 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
} }
// Animation behaviors NText {
Behavior on scale { text: "Notification History"
NumberAnimation { font.pointSize: Style.fontSizeL * scaling
duration: Style.animationSlow font.bold: true
easing.type: Easing.OutExpo 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 { NDivider {
NumberAnimation { Layout.fillWidth: true
duration: Style.animationNormal }
easing.type: Easing.OutQuad
} // Empty state when no notifications
} Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: NotificationService.historyModel.count === 0
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.centerIn: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling 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 { RowLayout {
Layout.fillWidth: true anchors {
fill: parent
margins: Style.marginM * scaling
}
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { // Notification content
text: "notifications" Column {
font.pointSize: Style.fontSizeXXL * scaling id: notificationContent
color: Color.mPrimary
}
NText {
text: "Notification History"
font.pointSize: Style.fontSizeL * scaling
font.bold: true
color: Color.mOnSurface
Layout.fillWidth: true 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 { NIconButton {
icon: "delete" icon: "delete"
tooltipText: "Clear History" tooltipText: "Delete Notification"
sizeMultiplier: 0.8 sizeMultiplier: 0.7
onClicked: NotificationService.clearHistory()
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
onClicked: { onClicked: {
notificationPanel.hide() Logger.log("NotificationHistory", "Removing notification:", summary)
NotificationService.historyModel.remove(index)
NotificationService.saveHistory()
} }
} }
} }
NDivider {} MouseArea {
id: notificationMouseArea
// Empty state when no notifications anchors.fill: parent
Item { anchors.rightMargin: Style.marginL * 3 * scaling
Layout.fillWidth: true hoverEnabled: 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
}
}
} }
}
ListView { ScrollBar.vertical: ScrollBar {
id: notificationList active: true
Layout.fillWidth: true anchors.right: parent.right
Layout.fillHeight: true anchors.top: parent.top
model: NotificationService.historyModel anchors.bottom: parent.bottom
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
}
}
} }
} }
} }

View file

@ -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()
}
}
}

View file

@ -8,9 +8,13 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NPanel {
id: root 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 // Tabs enumeration, order is NOT relevant
enum Tab { enum Tab {
About, About,
@ -28,344 +32,264 @@ NLoader {
} }
property int requestedTab: SettingsPanel.Tab.General property int requestedTab: SettingsPanel.Tab.General
property int currentTabIndex: 0
content: Component { Component {
NPanel { id: generalTab
id: panel 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 // When the panel opens, choose the appropriate tab
function hide() { onOpened: {
// Start hide animation var initialIndex = SettingsPanel.Tab.General
bgRect.scaleValue = 0.8 if (root.requestedTab !== null) {
bgRect.opacityValue = 0.0 for (var i = 0; i < root.tabsModel.length; i++) {
// Hide after animation completes if (root.tabsModel[i].id === root.requestedTab) {
hideTimer.start() initialIndex = i
} break
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: panel
function onDismissed() {
hide()
} }
} }
}
// Now that the UI is settled, set the current tab index.
root.currentTabIndex = initialIndex
}
// Timer to hide panel after animation panelContent: Rectangle {
Timer { anchors.fill: parent
id: hideTimer anchors.margins: Style.marginL * scaling
interval: Style.animationSlow color: Color.transparent
repeat: false
onTriggered: {
panel.visible = false
panel.dismissed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand RowLayout {
anchors.fill: parent
Component { spacing: Style.marginM * scaling
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()
}
}
Rectangle { Rectangle {
id: bgRect id: sidebar
color: Color.mSurface Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling
radius: Style.radiusL * scaling Layout.fillHeight: true
color: Color.mSurfaceVariant
border.color: Color.mOutline border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
layer.enabled: true radius: Style.radiusM * scaling
width: Math.max(screen.width * 0.5, 1280) * scaling
height: Math.max(screen.height * 0.5, 720) * scaling
anchors.centerIn: parent
// Animation properties Column {
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 {
anchors.fill: parent anchors.fill: parent
} anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
Behavior on scale { Repeater {
NumberAnimation { id: sections
duration: Style.animationSlow model: root.tabsModel
easing.type: Easing.OutExpo delegate: Rectangle {
} id: tabItem
} width: parent.width
Behavior on opacity { height: 32 * scaling
NumberAnimation { radius: Style.radiusS * scaling
duration: Style.animationNormal color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
easing.type: Easing.OutQuad readonly property bool selected: index === currentTabIndex
} property bool hovering: false
} property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
RowLayout {
RowLayout { anchors.fill: parent
anchors.fill: parent anchors.leftMargin: Style.marginS * scaling
anchors.margins: Style.marginL * scaling anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginL * scaling spacing: Style.marginS * scaling
// Tab icon on the left side
Rectangle { NIcon {
id: sidebar text: modelData.icon
Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling color: tabTextColor
Layout.fillHeight: true font.pointSize: Style.fontSizeL * scaling
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
}
} }
// 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 // Content
Rectangle { Rectangle {
id: contentPane 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.fillWidth: true
Layout.fillHeight: 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 clip: true
ColumnLayout { Repeater {
id: contentLayout model: root.tabsModel
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
RowLayout { onItemAdded: function (index, item) {
id: headerRow item.sourceComponent = root.tabsModel[index].source
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()
}
} }
NDivider { delegate: Loader {
Layout.fillWidth: true // All loaders will occupy the same space, stacked on top of each other.
} anchors.fill: parent
visible: index === root.currentTabIndex
Item { // The loader is only active (and uses memory) when its page is visible.
Layout.fillWidth: true active: visible
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
}
}
} }
} }
} }

View file

@ -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 { NDivider {
@ -199,6 +220,47 @@ ColumnLayout {
} }
} }
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginM * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Bar Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => {
Settings.data.audio.showMiniplayerAlbumArt = checked
}
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => {
Settings.data.audio.showMiniplayerCava = checked
}
}
}
// Divider // Divider
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -49,34 +49,17 @@ Item {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true Layout.fillWidth: true
NLabel { NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size" label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)." description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
} minimum: 1
maximum: 50
RowLayout { value: Settings.data.brightness.brightnessStep
Layout.fillWidth: true stepSize: 1
spacing: Style.marginM * scaling suffix: "%"
onValueChanged: {
NSlider { Settings.data.brightness.brightnessStep = value
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
} }
} }
} }

View file

@ -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
}
}
}
} }
} }
} }

View file

@ -16,12 +16,13 @@ NBox {
// PowerProfiles service // PowerProfiles service
property var powerProfiles: PowerProfiles property var powerProfiles: PowerProfiles
readonly property bool hasPP: powerProfiles.hasPerformanceProfile readonly property bool hasPP: powerProfiles.hasPerformanceProfile
property real spacing: 0
RowLayout { RowLayout {
id: powerRow id: powerRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: sidePanel.cardSpacing spacing: spacing
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }

View file

@ -61,7 +61,7 @@ NBox {
tooltipText: "Open Settings" tooltipText: "Open Settings"
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.isLoaded = !settingsPanel.isLoaded settingsPanel.open(screen)
} }
} }
@ -70,18 +70,22 @@ NBox {
icon: "power_settings_new" icon: "power_settings_new"
tooltipText: "Power Menu" tooltipText: "Power Menu"
onClicked: { 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 // Uptime
Timer { Timer {

View file

@ -9,6 +9,9 @@ import qs.Widgets
// Utilities: record & wallpaper // Utilities: record & wallpaper
NBox { NBox {
property real spacing: 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 1 Layout.preferredWidth: 1
implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling
@ -16,7 +19,7 @@ NBox {
id: utilRow id: utilRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: sidePanel.cardSpacing spacing: spacing
Item { Item {
Layout.fillWidth: true 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 // Wallpaper
NIconButton { NIconButton {
icon: "image" icon: "image"
tooltipText: "Open Wallpaper Selector" tooltipText: "Open Wallpaper Selector"
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }

View file

@ -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
}
}

View file

@ -7,205 +7,76 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NPanel {
id: root id: panel
// X coordinate on screen (in pixels) where the panel should align its center. panelWidth: 460 * scaling
// Set via openAt(x) from the bar button. panelHeight: 700 * scaling
property real anchorX: 0 panelAnchorRight: true
// Target screen to open on
property var targetScreen: null
function openAt(x, screen) { panelContent: Item {
anchorX = x id: content
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
}
}
content: Component { property real cardSpacing: Style.marginL * scaling
NPanel {
id: sidePanel
// Single source of truth for spacing between cards (both axes) anchors.left: parent.left
property real cardSpacing: Style.marginL * scaling anchors.right: parent.right
// X coordinate from the bar to align this panel under anchors.top: parent.top
property real anchorX: root.anchorX anchors.margins: content.cardSpacing
// Ensure this panel attaches to the intended screen implicitHeight: layout.implicitHeight
screen: root.targetScreen
// Override hide function to animate first // Layout content (not vertically anchored so implicitHeight is valid)
function hide() { ColumnLayout {
// Start hide animation id: layout
panelBackground.scaleValue = 0.8 // Use the same spacing value horizontally and vertically
panelBackground.opacityValue = 0.0 anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: content.cardSpacing
// Hide after animation completes // Cards (consistent inter-card spacing via ColumnLayout spacing)
hideTimer.start() 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 // Middle section: media + stats column
Connections { RowLayout {
target: sidePanel Layout.fillWidth: true
function onDismissed() { Layout.topMargin: 0
// Start hide animation Layout.bottomMargin: 0
panelBackground.scaleValue = 0.8 spacing: content.cardSpacing
panelBackground.opacityValue = 0.0
// Hide after animation completes // Media card
hideTimer.start() 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 // Bottom actions (two grouped rows of round buttons)
onVisibleChanged: { RowLayout {
if (!visible && panelBackground.opacityValue > 0) { Layout.fillWidth: true
// Start hide animation Layout.topMargin: 0
panelBackground.scaleValue = 0.8 Layout.bottomMargin: 0
panelBackground.opacityValue = 0.0 spacing: content.cardSpacing
// Hide after animation completes // Power Profiles switcher
hideTimer.start() PowerProfilesCard {
} spacing: content.cardSpacing
}
// 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
} }
// Timer to hide panel after animation // Utilities buttons
Timer { UtilitiesCard {
id: hideTimer spacing: content.cardSpacing
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 {}
}
}
} }
} }
} }

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Commons
Singleton { Singleton {
id: root id: root
@ -34,7 +35,7 @@ Singleton {
readonly property alias muted: root._muted readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted property bool _muted: !!sink?.audio?.muted
readonly property real stepVolume: 0.05 readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
PwObjectTracker { PwObjectTracker {
objects: [...root.sinks, ...root.sources] objects: [...root.sinks, ...root.sources]

View file

@ -27,6 +27,15 @@ Singleton {
return methods 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<var> { function getDetectedDisplays(): list<var> {
return detectedDisplays return detectedDisplays
} }

View file

@ -9,7 +9,7 @@ Singleton {
id: root id: root
property var values: Array(barsCount).fill(0) property var values: Array(barsCount).fill(0)
property int barsCount: 32 property int barsCount: 24
property var config: ({ property var config: ({
"general": { "general": {
@ -37,7 +37,7 @@ Singleton {
Process { Process {
id: process id: process
stdinEnabled: true stdinEnabled: true
running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.isLoaded running: (Settings.data.audio.visualizerType !== "none") && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava)
command: ["cava", "-p", "/dev/stdin"] command: ["cava", "-p", "/dev/stdin"]
onExited: { onExited: {
stdinEnabled = true stdinEnabled = true

View file

@ -11,12 +11,40 @@ Singleton {
property var history: [] property var history: []
property bool initialized: false property bool initialized: false
property int maxHistory: 50 // Limit clipboard history entries
// Internal state // Internal state
property bool _enabled: true 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 { Timer {
interval: 1000 interval: 2000
repeat: true repeat: true
running: root._enabled running: root._enabled
onTriggered: root.refresh() onTriggered: root.refresh()
@ -32,14 +60,17 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t) 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/')) const imageType = currentTypes.find(t => t.startsWith('image/'))
if (imageType) { if (imageType) {
imageProcess.mimeType = imageType imageProcess.mimeType = imageType
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
imageProcess.running = true imageProcess.running = true
} else {
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.running = true
} }
} else { } else {
typeProcess.isLoading = false typeProcess.isLoading = false
@ -65,17 +96,32 @@ Singleton {
"timestamp": new Date().getTime() "timestamp": new Date().getTime()
} }
// Check if this exact image already exists
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
if (!exists) { 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) { if (!textProcess.isLoading) {
root.initialized = true root.initialized = true
typeProcess.isLoading = false
} }
typeProcess.isLoading = false
} }
stdout: StdioCollector {} stdout: StdioCollector {}
@ -87,15 +133,18 @@ Singleton {
property bool isLoading: false property bool isLoading: false
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
textProcess.isLoading = false
if (exitCode === 0) { if (exitCode === 0) {
const content = String(stdout.text).trim() const content = String(stdout.text).trim()
if (content) { if (content && content.length > 0) {
const entry = { const entry = {
"type": 'text', "type": 'text',
"content": content, "content": content,
"timestamp": new Date().getTime() "timestamp": new Date().getTime()
} }
// Check if this exact text content already exists
const exists = root.history.find(item => { const exists = root.history.find(item => {
if (item.type === 'text') { if (item.type === 'text') {
return item.content === content return item.content === content
@ -104,36 +153,76 @@ Singleton {
}) })
if (!exists) { if (!exists) {
const newHistory = root.history.map(item => { // Normalize existing history entries
if (typeof item === 'string') { const normalizedHistory = root.history.map(item => {
return { if (typeof item === 'string') {
"type": 'text', return {
"content": item, "type": 'text',
"timestamp": new Date().getTime() "content": item,
} "timestamp": new Date().getTime(
} ) - 1000 // Make it slightly older
return item }
}) }
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 root.initialized = true
typeProcess.isLoading = false if (!imageProcess.running) {
typeProcess.isLoading = false
}
} }
stdout: StdioCollector {} stdout: StdioCollector {}
} }
function refresh() { function refresh() {
if (!typeProcess.isLoading && !textProcess.isLoading) { if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) {
typeProcess.isLoading = true typeProcess.isLoading = true
typeProcess.command = ["wl-paste", "-l"] typeProcess.command = ["wl-paste", "-l"]
typeProcess.running = true 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()
}
} }

View file

@ -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 // Generic logout/shutdown commands
function logout() { function logout() {
if (isHyprland) { if (isHyprland) {
@ -415,22 +434,15 @@ Singleton {
} }
} }
// Get current workspace function shutdown() {
function getCurrentWorkspace() { Quickshell.execDetached(["shutdown", "-h", "now"])
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) {
return ws
}
}
return null
} }
// Get focused window function reboot() {
function getFocusedWindow() { Quickshell.execDetached(["reboot"])
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { }
return windows[focusedWindowIndex]
} function suspend() {
return null Quickshell.execDetached(["systemctl", "suspend"])
} }
} }

View file

@ -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()
}
}

View file

@ -5,8 +5,16 @@ import Quickshell
Singleton { Singleton {
id: root id: root
// A ref. to the sidePanel, so it's accessible from other services
property var sidePanel: null
// Currently opened panel // Currently opened panel
property var openedPanel: null property var openedPanel: null
property var sidePanel: null function registerOpen(panel) {
if (openedPanel && openedPanel != panel) {
openedPanel.close()
}
openedPanel = panel
}
} }

View file

@ -9,7 +9,15 @@ Singleton {
// ------------------------------------------- // -------------------------------------------
// Manual scaling via Settings // Manual scaling via Settings
function scale(aScreen) { 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) { function scaleByName(aScreenName) {

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets
// Compact circular statistic display used in the SidePanel // Compact circular statistic display used in the SidePanel
Rectangle { Rectangle {
@ -73,7 +74,7 @@ Rectangle {
} }
// Percent centered in the circle // Percent centered in the circle
Text { NText {
id: valueLabel id: valueLabel
anchors.centerIn: parent anchors.centerIn: parent
text: `${root.value}${root.suffix}` text: `${root.value}${root.suffix}`

View file

@ -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
}
}
}

View file

@ -4,111 +4,179 @@ import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
PanelWindow { Loader {
id: root id: root
active: false
asynchronous: true
readonly property real scaling: ScalingService.scale(screen) readonly property real scaling: ScalingService.scale(screen)
property ShellScreen screen
property bool showOverlay: Settings.data.general.dimDesktop property Component panelContent: null
property int topMargin: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 property int panelWidth: 1500
property int bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 property int panelHeight: 400
// Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) property bool panelAnchorCentered: false
property color overlayColor: (showOverlay && (PanelService.openedPanel === root property bool panelAnchorLeft: false
|| isTransitioning)) ? Color.applyOpacity(Color.mShadow, property bool panelAnchorRight: false
"AA") : Color.transparent
property bool isTransitioning: false
signal dismissed
function hide() { // Animation properties
// Clear the panel service when hiding readonly property real originalScale: 0.7
if (PanelService.openedPanel === root) { readonly property real originalOpacity: 0.0
PanelService.openedPanel = null property real scaleValue: originalScale
} property real opacityValue: originalOpacity
isTransitioning = false
visible = false
root.dismissed()
}
function show() { property alias isClosing: hideTimer.running
// 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) {
// ignore signal opened
signal closed
// -----------------------------------------
function toggle(aScreen) {
if (!active || isClosing) {
open(aScreen)
} else {
close()
} }
} }
implicitWidth: screen.width // -----------------------------------------
implicitHeight: screen.height function open(aScreen) {
color: visible ? overlayColor : Color.transparent if (aScreen !== null) {
visible: false screen = aScreen
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
} }
// 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 { Timer {
id: showTimer id: hideTimer
interval: 50 // Small delay to ensure smooth transition interval: Style.animationSlow
repeat: false repeat: false
onTriggered: { onTriggered: {
PanelService.openedPanel = root closeCompleted()
isTransitioning = false
visible = true
} }
} }
Component.onDestruction: { // -----------------------------------------
try { sourceComponent: Component {
if (visible && Settings.openPanel === root) PanelWindow {
Settings.openPanel = null id: panelWindow
} catch (e) {
} visible: true
}
onVisibleChanged: { // Dim desktop if required
try { color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Color.applyOpacity(
if (!visible) { Color.mShadow,
// Clear panel service when panel becomes invisible "BB") : Color.transparent
if (PanelService.openedPanel === root) {
PanelService.openedPanel = null 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
}
}
} }
} }
} }

203
Widgets/NSpinBox.qml Normal file
View file

@ -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
}
}
}
}
}

View file

@ -6,7 +6,7 @@ import qs.Widgets
Text { Text {
id: root id: root
font.family: Settings.data.ui.fontFamily font.family: Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
color: Color.mOnSurface color: Color.mOnSurface

View file

@ -17,14 +17,15 @@ import qs.Modules.AppLauncher
import qs.Modules.Background import qs.Modules.Background
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.DemoPanel
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.IPC import qs.Modules.IPC
import qs.Modules.LockScreen import qs.Modules.LockScreen
import qs.Modules.Notification import qs.Modules.Notification
import qs.Modules.SettingsPanel import qs.Modules.SettingsPanel
import qs.Modules.PowerPanel
import qs.Modules.SidePanel import qs.Modules.SidePanel
import qs.Modules.Toast import qs.Modules.Toast
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -41,10 +42,6 @@ ShellRoot {
id: appLauncherPanel id: appLauncherPanel
} }
DemoPanel {
id: demoPanel
}
SidePanel { SidePanel {
id: sidePanel id: sidePanel
} }
@ -69,6 +66,10 @@ ShellRoot {
id: lockScreen id: lockScreen
} }
PowerPanel {
id: powerPanel
}
ToastManager {} ToastManager {}
IPCManager {} IPCManager {}