Merge branch 'noctalia-dev:main' into main

This commit is contained in:
DrWeb56
2025-11-08 11:54:50 +05:00
committed by GitHub
33 changed files with 629 additions and 774 deletions
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Die Paneele rasten an der Stange und den Bildschirmrändern ein und sorgen so für ein nahtloses Erscheinungsbild mit stilvollen, umgekehrten Ecken.",
"label": "Paneele an Kanten ausrichten"
},
"settings-panel-attached-to-bar": {
"description": "Richten Sie das Einstellungsfenster an der Leiste aus, um ein einheitliches Erscheinungsbild zu erhalten.",
"label": "Einstellungsfenster an Leiste ausrichten"
},
"panels-overlay": {
"description": "Stellt sicher, dass Panels und die Leiste sichtbar bleiben, auch über Vollbildanwendungen.",
"label": "Panels & Leiste oben behalten"
@@ -1528,6 +1524,10 @@
"description": "Passen Sie das Aussehen, die Haptik und das Verhalten der Benutzeroberfläche an.",
"label": "Aussehen"
},
"settings-panel-attached-to-bar": {
"description": "Richten Sie das Einstellungsfenster an der Leiste aus, um ein einheitliches Erscheinungsbild zu erhalten.",
"label": "Einstellungsfenster an Leiste ausrichten"
},
"shadows": {
"description": "Aktiviert Schlagschatten unter Balken und Panels.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Normaler Modus",
"not-installed": "wlsunset nicht installiert"
},
"noctalia-performance": {
"disabled": "Leistungsmodus deaktiviert",
"enabled": "Leistungsmodus aktiviert",
"label": "Noctalia-Aufführung"
},
"power-profile": {
"changed": "Energieprofil geändert",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Panels lock to the bar and screen edges, creating a seamless look with stylish inverted corners.",
"label": "Snap panels to edges"
},
"settings-panel-attached-to-bar": {
"description": "Keep the Settings window aligned with the bar for a unified look.",
"label": "Snap Settings window to bar"
},
"panels-overlay": {
"description": "Ensures panels and the bar remain visible, even over fullscreen applications.",
"label": "Keep panels & bar on top"
@@ -1528,6 +1524,10 @@
"description": "Customize the look, feel, and behavior of the interface.",
"label": "Appearance"
},
"settings-panel-attached-to-bar": {
"description": "Keep the Settings window aligned with the bar for a unified look.",
"label": "Snap Settings window to bar"
},
"shadows": {
"description": "Enables drop shadows under bars and panels.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Normal mode",
"not-installed": "wlsunset not installed"
},
"noctalia-performance": {
"disabled": "Performance mode disabled",
"enabled": "Performance mode enabled",
"label": "Noctalia performance"
},
"power-profile": {
"changed": "Power profile changed",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Los paneles se fijan a la barra y a los bordes de la pantalla, creando una apariencia impecable con elegantes esquinas invertidas.",
"label": "Ajustar paneles a los bordes"
},
"settings-panel-attached-to-bar": {
"description": "Mantén la ventana de Configuración alineada con la barra para un aspecto unificado.",
"label": "Ajustar Configuración a la barra"
},
"panels-overlay": {
"description": "Garantiza que los paneles y la barra permanezcan visibles, incluso sobre aplicaciones en pantalla completa.",
"label": "Mantener paneles y barra en la parte superior"
@@ -1528,6 +1524,10 @@
"description": "Personaliza la apariencia, el ambiente y el comportamiento de la interfaz.",
"label": "Apariencia"
},
"settings-panel-attached-to-bar": {
"description": "Mantén la ventana de Configuración alineada con la barra para un aspecto unificado.",
"label": "Ajustar Configuración a la barra"
},
"shadows": {
"description": "Habilita sombras paralelas debajo de las barras y los paneles.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Modo normal",
"not-installed": "wlsunset no está instalado"
},
"noctalia-performance": {
"disabled": "Modo de rendimiento desactivado",
"enabled": "Modo de rendimiento activado",
"label": "Actuación de Noctalia"
},
"power-profile": {
"changed": "Perfil de energía cambiado",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Les panneaux se verrouillent sur la barre et les bords de l'écran, créant un aspect homogène avec d'élégants coins inversés.",
"label": "Aligner les panneaux aux bords"
},
"settings-panel-attached-to-bar": {
"description": "Alignez la fenêtre des paramètres sur la barre pour un rendu cohérent.",
"label": "Aligner la fenêtre des paramètres sur la barre"
},
"panels-overlay": {
"description": "Garantit que les panneaux et la barre restent visibles, même au-dessus des applications en plein écran.",
"label": "Garder les panneaux et la barre en haut."
@@ -1528,6 +1524,10 @@
"description": "Personnaliser l'apparence, l'ergonomie et le comportement de l'interface.",
"label": "Apparence"
},
"settings-panel-attached-to-bar": {
"description": "Alignez la fenêtre des paramètres sur la barre pour un rendu cohérent.",
"label": "Aligner la fenêtre des paramètres sur la barre"
},
"shadows": {
"description": "Active les ombres portées sous les barres et les panneaux.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Mode normal",
"not-installed": "wlsunset non installé"
},
"noctalia-performance": {
"disabled": "Mode performance désactivé",
"enabled": "Mode performance activé",
"label": "Performance de Noctalia"
},
"power-profile": {
"changed": "Profil d'alimentation changé",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Os painéis travam na barra e nas bordas da tela, criando uma aparência perfeita com cantos invertidos elegantes.",
"label": "Ajustar Painéis às Bordas"
},
"settings-panel-attached-to-bar": {
"description": "Mantenha a janela de Configurações alinhada com a barra para um visual uniforme.",
"label": "Ajustar Configurações à barra"
},
"panels-overlay": {
"description": "Garante que os painéis e a barra permaneçam visíveis, mesmo sobre aplicativos em tela cheia.",
"label": "Manter Painéis e Barra no Topo"
@@ -1528,6 +1524,10 @@
"description": "Personalize a aparência, a sensação e o comportamento da interface.",
"label": "Aparência"
},
"settings-panel-attached-to-bar": {
"description": "Mantenha a janela de Configurações alinhada com a barra para um visual uniforme.",
"label": "Ajustar Configurações à barra"
},
"shadows": {
"description": "Ativa sombras projetadas sob barras e painéis.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Modo normal",
"not-installed": "wlsunset não está instalado"
},
"noctalia-performance": {
"disabled": "Modo de desempenho desativado",
"enabled": "Modo de desempenho ativado",
"label": "Apresentação de Noctalia"
},
"power-profile": {
"changed": "Perfil de energia alterado",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Paneller çubuğa ve ekran kenarlarına kilitlenerek şık ters köşelerle kusursuz bir görünüm oluşturur.",
"label": "Kenarları Yakala"
},
"settings-panel-attached-to-bar": {
"description": "Ayarlar penceresini çubukla hizalayarak tutarlı bir görünüm sağlayın.",
"label": "Ayarlar penceresini çubuğa hizala"
},
"panels-overlay": {
"description": "Panellerin ve çubuğun, tam ekran uygulamalarda bile görünür kalmasını sağlar.",
"label": "Panelleri ve Çubuğu Üstte Tut"
@@ -1528,6 +1524,10 @@
"description": "Arayüzün görünümünü, hissini ve davranışını özelleştirin.",
"label": "Görünüm"
},
"settings-panel-attached-to-bar": {
"description": "Ayarlar penceresini çubukla hizalayarak tutarlı bir görünüm sağlayın.",
"label": "Ayarlar penceresini çubuğa hizala"
},
"shadows": {
"description": "Çubukların ve panellerin altında gölgelerin etkinleştirilmesini sağlar.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Normal mod",
"not-installed": "wlsunset kurulu değil"
},
"noctalia-performance": {
"disabled": "Performans modu devre dışı bırakıldı",
"enabled": "Performans modu etkinleştirildi",
"label": "Noctalia performansı"
},
"power-profile": {
"changed": "Güç profili değişti",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "Панелі фіксуються до штанги та країв екрана, створюючи бездоганний вигляд зі стильними перевернутими кутами.",
"label": "Прив'язувати панелі до країв"
},
"settings-panel-attached-to-bar": {
"description": "Вирівнюйте вікно налаштувань відносно панелі, щоб зберегти цілісний вигляд.",
"label": "Прив'язувати вікно налаштувань до панелі"
},
"panels-overlay": {
"description": "Забезпечує видимість панелей і панелі завдань, навіть поверх повноекранних програм.",
"label": "Тримати панелі та панель зверху"
@@ -1528,6 +1524,10 @@
"description": "Налаштуйте вигляд, відчуття та поведінку інтерфейсу.",
"label": "Зовнішній вигляд"
},
"settings-panel-attached-to-bar": {
"description": "Вирівнюйте вікно налаштувань відносно панелі, щоб зберегти цілісний вигляд.",
"label": "Прив'язувати вікно налаштувань до панелі"
},
"shadows": {
"description": "Увімкнути тіні під панелями та смугами.",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "Звичайний режим",
"not-installed": "wlsunset не встановлено"
},
"noctalia-performance": {
"disabled": "Режим продуктивності вимкнено.",
"enabled": "Режим продуктивності ввімкнено.",
"label": "Виступ Noctalia"
},
"power-profile": {
"changed": "Профіль живлення змінено",
"profile-name": "\"{profile}\""
+9 -4
View File
@@ -1511,10 +1511,6 @@
"description": "面板锁定到边栏和屏幕边缘,形成无缝外观,并具有时尚的倒角。",
"label": "将面板对齐到边缘"
},
"settings-panel-attached-to-bar": {
"description": "使设置窗口与边栏保持对齐,实现统一的外观。",
"label": "将设置窗口贴合边栏"
},
"panels-overlay": {
"description": "确保面板和栏保持可见,即使在全屏应用程序上也是如此。",
"label": "将面板和栏保持在顶部"
@@ -1528,6 +1524,10 @@
"description": "自定义界面的外观、感觉和行为。",
"label": "外观"
},
"settings-panel-attached-to-bar": {
"description": "使设置窗口与边栏保持对齐,实现统一的外观。",
"label": "将设置窗口贴合边栏"
},
"shadows": {
"description": "启用条形图和面板下的阴影。",
"direction": {
@@ -1734,6 +1734,11 @@
"normal": "正常模式",
"not-installed": "未安装 wlsunset"
},
"noctalia-performance": {
"disabled": "性能模式已禁用",
"enabled": "已启用性能模式",
"label": "夜之颂演出"
},
"power-profile": {
"changed": "电源模式已更改",
"profile-name": "\"{profile}\""
+8 -6
View File
@@ -4,13 +4,15 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
/*
Preset sizes for font, radii, ?
*/
*/
// Font size
property real fontSizeXXS: 8
@@ -65,11 +67,11 @@ Singleton {
property real shadowVerticalOffset: Settings.data.general.shadowOffsetY
// Animation duration (ms)
property int animationFaster: Settings.data.general.animationDisabled ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
property int animationFast: Settings.data.general.animationDisabled ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: Settings.data.general.animationDisabled ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: Settings.data.general.animationDisabled ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Settings.data.general.animationDisabled ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
// Delays
property int tooltipDelay: 300
+47 -13
View File
@@ -17,6 +17,7 @@ Loader {
required property ShellScreen modelData
property string wallpaper: ""
property bool overviewIsSeen: false
Component.onCompleted: {
if (modelData) {
@@ -25,6 +26,13 @@ Loader {
setWallpaperInitial()
}
Component.onDestruction: {
// Clean up resources to prevent memory leak when overviewEnabled is toggled off
timerDisableFx.stop()
bgImage.layer.enabled = false
bgImage.source = ""
}
// External state management
Connections {
target: WallpaperService
@@ -35,6 +43,27 @@ Loader {
}
}
Connections {
target: CompositorService.backend
function onOverviewActiveChanged() {
if (CompositorService.backend.overviewActive) {
overviewIsSeen = true
timerDisableFx.stop()
} else {
timerDisableFx.restart()
}
}
}
// Use to disable effects when overview is no longer necessary.
Timer {
id: timerDisableFx
interval: 2000
onTriggered: {
overviewIsSeen = false
}
}
function setWallpaperInitial() {
if (!WallpaperService || !WallpaperService.isInitialized) {
Qt.callLater(setWallpaperInitial)
@@ -64,23 +93,28 @@ Loader {
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: wallpaper
smooth: true
smooth: false
mipmap: false
cache: false
asynchronous: true
// Image is heavily blurred, so might as well save a lot of memory here.
sourceSize: Qt.size(1280, 720)
}
visible: overviewIsSeen
MultiEffect {
anchors.fill: parent
source: bgImage
autoPaddingEnabled: false
blurEnabled: true
blur: 1.0
blurMax: 64
colorization: Style.opacityMedium
colorizationColor: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface
// Image is heavily blurred, so use low resolution to save memory
sourceSize: Qt.size(1280, 720)
layer.enabled: overviewIsSeen
layer.smooth: false
layer.effect: MultiEffect {
blurEnabled: true
blur: 1.0
blurMax: 32
}
Rectangle {
anchors.fill: parent
color: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface
opacity: 0.8
}
}
}
}
+2 -2
View File
@@ -54,8 +54,8 @@ Item {
property bool effectsActive: false
property color effectColor: Color.mPrimary
property int horizontalPadding: Math.round(Style.marginS)
property int spacingBetweenPills: Math.round(Style.marginXS)
property int horizontalPadding: Style.marginS
property int spacingBetweenPills: Style.marginXS
// Wheel scroll handling
property int wheelAccumulatedDelta: 0
+11 -30
View File
@@ -78,7 +78,9 @@ PanelWindow {
// Desktop dimming when panels are open
property bool dimDesktop: Settings.data.general.dimDesktop
property bool isPanelOpen: PanelService.openedPanel !== null
property bool isPanelOpen: (PanelService.openedPanel !== null) && (PanelService.openedPanel.screen === screen)
property bool isPanelClosing: (PanelService.openedPanel !== null) && PanelService.openedPanel.isClosing
color: {
if (dimDesktop && isPanelOpen) {
return Qt.alpha(Color.mShadow, 0.8)
@@ -124,30 +126,8 @@ PanelWindow {
id: backgroundMaskRegion
x: 0
y: 0
width: root.isPanelOpen ? root.width : 0
height: root.isPanelOpen ? root.height : 0
intersection: Intersection.Subtract
}
}
// Variants for panel regions
Variants {
id: panelRegions
// Model is the list of open panels (filters out closed ones)
model: [audioPanel, controlCenterPanel, calendarPanel].filter(function (p) {
return p && p.isPanelOpen
})
Region {
required property var modelData
property var panelBg: modelData.panelRegion
// Direct bindings
x: panelBg?.x ?? 0
y: panelBg?.y ?? 0
width: panelBg?.width ?? 0
height: panelBg?.height ?? 0
width: root.isPanelOpen && !isPanelClosing ? root.width : 0
height: root.isPanelOpen && !isPanelClosing ? root.height : 0
intersection: Intersection.Subtract
}
}
@@ -174,11 +154,12 @@ PanelWindow {
MouseArea {
anchors.fill: parent
enabled: root.isPanelOpen
onClicked: {
if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (PanelService.openedPanel) {
PanelService.openedPanel.close()
}
}
z: 0 // Behind panels and bar
}
+39 -23
View File
@@ -58,23 +58,38 @@ Variants {
readonly property bool isRight: location.endsWith("_right")
readonly property bool isCentered: location === "top" || location === "bottom"
readonly property string barPos: Settings.data.bar.position
readonly property bool isFloating: Settings.data.bar.floating
readonly property int notifWidth: Math.round(420 * Style.uiScaleRatio)
// Calculate bar offset once
readonly property int barOffset: {
const barPos = Settings.data.bar.position
const isFloating = Settings.data.bar.floating
// Calculate bar offsets for each edge separately
readonly property int barOffsetTop: {
if (barPos !== "top")
return 0
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
return Style.barHeight + floatMarginV
}
// Check if bar is on same edge as notifications
if ((isTop && barPos === "top") || (isBottom && barPos === "bottom")) {
return Style.barHeight + floatMarginV
}
if ((isLeft && barPos === "left") || (isRight && barPos === "right")) {
return Style.barHeight + floatMarginH
}
return 0
readonly property int barOffsetBottom: {
if (barPos !== "bottom")
return 0
const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0
return Style.barHeight + floatMarginV
}
readonly property int barOffsetLeft: {
if (barPos !== "left")
return 0
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
return floatMarginH
}
readonly property int barOffsetRight: {
if (barPos !== "right")
return 0
const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
return floatMarginH
}
// Anchoring
@@ -83,11 +98,11 @@ Variants {
anchors.left: isLeft
anchors.right: isRight
// Margins for PanelWindow (not anchors.topMargin!)
margins.top: isTop ? Style.marginM + barOffset : 0
margins.bottom: isBottom ? Style.marginM + barOffset : 0
margins.left: isLeft ? Style.marginM + barOffset : 0
margins.right: isRight ? Style.marginM + barOffset : 0
// Margins for PanelWindow - only apply bar offset for the specific edge where the bar is
margins.top: isTop ? Style.marginM + barOffsetTop : 0
margins.bottom: isBottom ? Style.marginM + barOffsetBottom : 0
margins.left: isLeft ? Style.marginM + barOffsetLeft : 0
margins.right: isRight ? Style.marginM + barOffsetRight : 0
implicitWidth: notifWidth
implicitHeight: notificationStack.implicitHeight + Style.marginL
@@ -186,7 +201,6 @@ Variants {
id: cardBackground
anchors.fill: parent
anchors.margins: Style.marginM
anchors.rightMargin: Style.marginXL
radius: Style.radiusL
border.color: Qt.alpha(Color.mOutline, Settings.data.notifications.backgroundOpacity || 1.0)
border.width: Style.borderS
@@ -373,7 +387,6 @@ Variants {
id: notificationContent
anchors.fill: parent
anchors.margins: Style.marginM
anchors.rightMargin: Style.marginXL
spacing: Style.marginM
RowLayout {
@@ -433,10 +446,11 @@ Variants {
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
Layout.fillWidth: true
Layout.rightMargin: Style.marginM
}
NText {
@@ -445,10 +459,12 @@ Variants {
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 5
elide: Text.ElideRight
visible: text.length > 0
Layout.fillWidth: true
Layout.rightMargin: Style.marginXL
}
// Actions
@@ -508,7 +524,7 @@ Variants {
anchors.top: parent.top
anchors.topMargin: Style.marginXL
anchors.right: parent.right
anchors.rightMargin: Style.marginXL * 1.5
anchors.rightMargin: Style.marginXL
onClicked: {
NotificationService.removeFromHistory(model.id)
+167 -210
View File
@@ -11,10 +11,14 @@ import qs.Widgets
NBox {
id: root
// Track whether we have an active media player
readonly property bool hasActivePlayer: MediaService.currentPlayer && MediaService.canPlay
// Wrapper - rounded rect clipper
Item {
anchors.fill: parent
layer.enabled: true
layer.smooth: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
@@ -34,16 +38,9 @@ NBox {
readonly property int dim: Math.round(256 * Style.uiScaleRatio)
id: bgImage
anchors.fill: parent
source: MediaService.trackArtUrl || WallpaperService.getWallpaper(Screen.name)
source: MediaService.trackArtUrl || WallpaperService.getWallpaper(screen.name)
sourceSize: Qt.size(dim, dim)
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.25
blurMax: 16
}
}
// Dark overlay for readability
@@ -189,245 +186,205 @@ NBox {
anchors.fill: parent
anchors.margins: Style.marginM
// No media player detected
ColumnLayout {
id: fallback
// No media player detected - use Loader for performance
Loader {
id: fallbackLoader
Layout.fillWidth: true
Layout.fillHeight: true
active: !root.hasActivePlayer
visible: !main.visible
spacing: Style.marginS
sourceComponent: ColumnLayout {
spacing: Style.marginS
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Item {
Layout.fillWidth: true
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
Item {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Style.fontSizeXXXL * 4
Layout.preferredHeight: Style.fontSizeXXXL * 4
Item {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Style.fontSizeXXXL * 4
Layout.preferredHeight: Style.fontSizeXXXL * 4
// Pulsating audio circles (background)
Repeater {
model: 3
Rectangle {
// Center icon (static, no animation to avoid GPU cost)
NIcon {
anchors.centerIn: parent
width: parent.width * (1.0 + index * 0.2)
height: width
radius: width / 2
color: "transparent"
border.color: Color.mOnSurfaceVariant
border.width: 2
opacity: 0
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 1.0
to: 0
duration: 2000
easing.type: Easing.OutQuad
}
}
SequentialAnimation on scale {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: index * 600
}
NumberAnimation {
from: 0.6
to: 1.2
duration: 2000
easing.type: Easing.OutQuad
}
}
icon: "disc"
pointSize: Style.fontSizeXXXL * 3
color: Color.mOnSurfaceVariant
opacity: 1.0
}
}
// Spinning disc
NIcon {
anchors.centerIn: parent
icon: "disc"
pointSize: Style.fontSizeXXXL * 3
color: Color.mOnSurfaceVariant
}
}
// Descriptive text
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXS
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
// MediaPlayer Main Content
ColumnLayout {
id: main
// MediaPlayer Main Content - use Loader for performance
Loader {
id: mainLoader
Layout.fillWidth: true
Layout.fillHeight: true
active: root.hasActivePlayer
visible: MediaService.currentPlayer && MediaService.canPlay
spacing: Style.marginS
sourceComponent: ColumnLayout {
id: main
spacing: Style.marginS
// Spacer to push content down
Item {
Layout.preferredHeight: Style.marginM
}
// Spacer to push content down
Item {
Layout.preferredHeight: Style.marginM
}
// Metadata at the bottom left
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
spacing: Style.marginXS
NText {
visible: MediaService.trackTitle !== ""
text: MediaService.trackTitle
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
// Metadata at the bottom left
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
spacing: Style.marginXS
NText {
visible: MediaService.trackTitle !== ""
text: MediaService.trackTitle
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
Layout.fillWidth: true
}
NText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Color.mPrimary
pointSize: Style.fontSizeS
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
visible: MediaService.trackAlbum !== ""
text: MediaService.trackAlbum
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
elide: Text.ElideRight
Layout.fillWidth: true
}
}
NText {
visible: MediaService.trackArtist !== ""
text: MediaService.trackArtist
color: Color.mPrimary
pointSize: Style.fontSizeS
elide: Text.ElideRight
// Progress slider
Item {
id: progressWrapper
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
Layout.fillWidth: true
}
height: Style.baseWidgetSize * 0.5
NText {
visible: MediaService.trackAlbum !== ""
text: MediaService.trackAlbum
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
elide: Text.ElideRight
Layout.fillWidth: true
}
}
property real localSeekRatio: -1
property real lastSentSeekRatio: -1
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0
const r = MediaService.currentPosition / MediaService.trackLength
if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
}
property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
// Progress slider
Item {
id: progressWrapper
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
Layout.fillWidth: true
height: Style.baseWidgetSize * 0.5
property real localSeekRatio: -1
property real lastSentSeekRatio: -1
property real seekEpsilon: 0.01
property real progressRatio: {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0
const r = MediaService.currentPosition / MediaService.trackLength
if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
}
property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
}
}
}
}
}
NSlider {
id: progressSlider
anchors.fill: parent
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek
heightRatio: 0.6
NSlider {
id: progressSlider
anchors.fill: parent
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek
heightRatio: 0.6
onMoved: {
progressWrapper.localSeekRatio = value
seekDebounce.restart()
}
onPressedChanged: {
if (pressed) {
MediaService.isSeeking = true
onMoved: {
progressWrapper.localSeekRatio = value
MediaService.seekByRatio(value)
progressWrapper.lastSentSeekRatio = value
} else {
seekDebounce.stop()
MediaService.seekByRatio(value)
MediaService.isSeeking = false
progressWrapper.localSeekRatio = -1
progressWrapper.lastSentSeekRatio = -1
seekDebounce.restart()
}
onPressedChanged: {
if (pressed) {
MediaService.isSeeking = true
progressWrapper.localSeekRatio = value
MediaService.seekByRatio(value)
progressWrapper.lastSentSeekRatio = value
} else {
seekDebounce.stop()
MediaService.seekByRatio(value)
MediaService.isSeeking = false
progressWrapper.localSeekRatio = -1
progressWrapper.lastSentSeekRatio = -1
}
}
}
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MediaService.isSeeking
}
}
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MediaService.isSeeking
}
}
// Spacer to push media controls down
Item {
Layout.preferredHeight: Style.marginL
}
// Media controls
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NIconButton {
icon: "media-prev"
visible: MediaService.canGoPrevious
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
// Spacer to push media controls down
Item {
Layout.preferredHeight: Style.marginL
}
NIconButton {
icon: MediaService.isPlaying ? "media-pause" : "media-play"
visible: (MediaService.canPlay || MediaService.canPause)
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
}
// Media controls
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NIconButton {
icon: "media-next"
visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {}
NIconButton {
icon: "media-prev"
visible: MediaService.canGoPrevious
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
}
NIconButton {
icon: MediaService.isPlaying ? "media-pause" : "media-play"
visible: (MediaService.canPlay || MediaService.canPause)
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
}
NIconButton {
icon: "media-next"
visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {}
}
}
}
}
@@ -134,11 +134,10 @@ NBox {
}
}
RowLayout {
visible: !weatherReady
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
NBusyIndicator {}
Loader {
active: !weatherReady
Layout.alignment: Qt.AlignCenter
sourceComponent: NBusyIndicator {}
}
}
}
@@ -144,16 +144,14 @@ SmartPanel {
property string notificationId: model.id
property bool isExpanded: notificationList.expandedId === notificationId
width: notificationList.width
height: notificationLayoutWrapper.height + (Style.marginM * 2)
// Cache the content height to break binding loops
property real contentHeight: notificationLayout.implicitHeight
Behavior on height {
enabled: !Settings.data.general.animationDisabled
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
// Cache truncation state to avoid binding to truncated property during polish
property bool hasTextTruncated: false
width: notificationList.width
height: contentHeight + (Style.marginM * 2)
Rectangle {
anchors.fill: parent
@@ -176,7 +174,7 @@ SmartPanel {
anchors.fill: parent
// Don't capture clicks on the delete button
anchors.rightMargin: 48
enabled: (summaryText.truncated || bodyText.truncated)
enabled: hasTextTruncated
onClicked: {
if (notificationList.expandedId === notificationId) {
notificationList.expandedId = ""
@@ -187,6 +185,25 @@ SmartPanel {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
// Update truncation state asynchronously
Connections {
target: summaryText
function onTruncatedChanged() {
hasTextTruncated = summaryText.truncated || bodyText.truncated
}
}
Connections {
target: bodyText
function onTruncatedChanged() {
hasTextTruncated = summaryText.truncated || bodyText.truncated
}
}
Component.onCompleted: {
hasTextTruncated = summaryText.truncated || bodyText.truncated
}
Item {
id: notificationLayoutWrapper
anchors.left: parent.left
@@ -200,22 +217,16 @@ SmartPanel {
width: parent.width
spacing: Style.marginM
// Icon column - use simple Item instead of ColumnLayout to avoid polish loop
Item {
// Icon - properly centered vertically
NImageCircled {
Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio)
Layout.alignment: Qt.AlignTop
NImageCircled {
anchors.top: parent.top
anchors.topMargin: 20
width: Math.round(40 * Style.uiScaleRatio)
height: Math.round(40 * Style.uiScaleRatio)
imagePath: model.cachedImage || model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio)
Layout.alignment: Qt.AlignVCenter
imagePath: model.cachedImage || model.originalImage || ""
borderColor: Color.transparent
borderWidth: 0
fallbackIcon: "bell"
fallbackIconSize: 24
}
// Notification content column
@@ -276,6 +287,11 @@ SmartPanel {
Layout.fillWidth: true
maximumLineCount: isExpanded ? 999 : 2
elide: Text.ElideRight
// Smooth transition without triggering layout recalculation
Behavior on maximumLineCount {
enabled: false // Disable animation on this to avoid polish loops
}
}
// Body
@@ -290,18 +306,24 @@ SmartPanel {
maximumLineCount: isExpanded ? 999 : 3
elide: Text.ElideRight
visible: text.length > 0
// Smooth transition without triggering layout recalculation
Behavior on maximumLineCount {
enabled: false // Disable animation on this to avoid polish loops
}
}
// Spacer for expand indicator
Item {
Layout.fillWidth: true
Layout.preferredHeight: (!isExpanded && (summaryText.truncated || bodyText.truncated)) ? (Style.marginS) : 0
Layout.preferredHeight: Style.marginS
visible: !isExpanded && hasTextTruncated
}
// Expand indicator
RowLayout {
Layout.fillWidth: true
visible: !isExpanded && (summaryText.truncated || bodyText.truncated)
visible: !isExpanded && hasTextTruncated
spacing: Style.marginXS
Item {
+78 -77
View File
@@ -12,13 +12,13 @@ import qs.Modules.MainScreen
SmartPanel {
id: root
preferredWidth: 820 * Style.uiScaleRatio
preferredHeight: 900 * Style.uiScaleRatio
preferredWidth: Math.round(820 * Style.uiScaleRatio)
preferredHeight: Math.round(900 * Style.uiScaleRatio)
readonly property bool attachToBar: Settings.data.ui.settingsPanelAttachToBar
readonly property string barPosition: Settings.data.bar.position
forceAttachToBar: attachToBar
panelAnchorHorizontalCenter: attachToBar ? (barPosition === "top" || barPosition === "bottom") : true
panelAnchorVerticalCenter: attachToBar ? (barPosition === "left" || barPosition === "right") : true
panelAnchorTop: attachToBar && barPosition === "top"
@@ -360,92 +360,93 @@ SmartPanel {
border.width: Style.borderS
radius: Style.radiusM
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton // Don't interfere with clicks
property int wheelAccumulator: 0
onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
}
ColumnLayout {
NListView {
id: sidebarList
anchors.fill: parent
anchors.margins: Style.marginS
model: root.tabsModel
spacing: Style.marginXS
currentIndex: root.currentTabIndex
verticalPolicy: ScrollBar.AsNeeded
Repeater {
id: sections
model: root.tabsModel
delegate: Rectangle {
id: tabItem
Layout.fillWidth: true
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginM * 2
radius: Style.radiusS
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface)
delegate: Rectangle {
id: tabItem
width: sidebarList.verticalScrollBarActive ? sidebarList.width - sidebarList.scrollBarWidth - Style.marginXS : sidebarList.width
height: tabEntryRow.implicitHeight + Style.marginM * 2
radius: Style.radiusS
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : Color.transparent)
readonly property bool selected: index === root.currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
Behavior on width {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
spacing: Style.marginM
// Tab icon
NIcon {
icon: modelData.icon
color: tabTextColor
pointSize: Style.fontSizeXL
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
// Tab label
NText {
text: I18n.tr(modelData.label)
color: tabTextColor
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
spacing: Style.marginM
// Tab icon
NIcon {
icon: modelData.icon
color: tabTextColor
pointSize: Style.fontSizeXL
}
// Tab label
NText {
text: I18n.tr(modelData.label)
color: tabTextColor
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: root.currentTabIndex = index
}
}
Item {
Layout.fillHeight: true
onCurrentIndexChanged: {
if (currentIndex !== root.currentTabIndex) {
root.currentTabIndex = currentIndex
}
}
Connections {
target: root
function onCurrentTabIndexChanged() {
if (sidebarList.currentIndex !== root.currentTabIndex) {
sidebarList.currentIndex = root.currentTabIndex
sidebarList.positionViewAtIndex(root.currentTabIndex, ListView.Contain)
}
}
}
}
}
@@ -255,7 +255,7 @@ ColumnLayout {
NSectionEditor {
sectionName: I18n.tr("settings.control-center.shortcuts.sectionLeft")
sectionId: "left"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
maxWidgets: 5
widgetRegistry: ControlCenterWidgetRegistry
widgetModel: Settings.data.controlCenter.shortcuts["left"]
@@ -274,7 +274,7 @@ ColumnLayout {
NSectionEditor {
sectionName: I18n.tr("settings.control-center.shortcuts.sectionRight")
sectionId: "right"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
maxWidgets: 5
widgetRegistry: ControlCenterWidgetRegistry
widgetModel: Settings.data.controlCenter.shortcuts["right"]
+2 -2
View File
@@ -11,8 +11,8 @@ import qs.Modules.MainScreen
SmartPanel {
id: root
preferredWidth: Math.round(520 * Style.uiScaleRatio)
preferredHeight: Math.round(600 * Style.uiScaleRatio)
preferredWidth: Math.round(preferredWidthRatio * 2560 * Style.uiScaleRatio)
preferredHeight: Math.round(preferredHeightRatio * 1440 * Style.uiScaleRatio)
preferredWidthRatio: 0.4
preferredHeightRatio: 0.6
+1 -1
View File
@@ -134,7 +134,7 @@ Singleton {
},
"SystemMonitor": {
"allowUserSettings": true,
"usePrimaryColor": true,
"usePrimaryColor": false,
"showCpuUsage": true,
"showCpuTemp": true,
"showMemoryUsage": true,
+13 -1
View File
@@ -40,7 +40,7 @@ Item {
function toggle() {
root.withTargetScreen(screen => {
var calendarPanel = PanelService.getPanel("calendarPanel", screen)
calendarPanel.toggle(null, "Clock")
calendarPanel?.toggle(null, "Clock")
})
}
}
@@ -287,6 +287,18 @@ Item {
break
}
}
function toggleNoctaliaPerformance() {
PowerProfileService.toggleNoctaliaPerformance()
}
function enableNoctaliaPerformance() {
PowerProfileService.setNoctaliaPerformance(true)
}
function disableNoctaliaPerformance() {
PowerProfileService.setNoctaliaPerformance(false)
}
}
IpcHandler {
+2
View File
@@ -38,6 +38,8 @@ Singleton {
var lines = []
lines.push("[config]")
lines.push("")
lines.push("[templates]")
lines.push("")
lines.push("# User-defined templates")
lines.push("# Add your custom templates below")
lines.push("# Example:")
+1 -1
View File
@@ -123,7 +123,7 @@ Singleton {
const data = createData(notification)
addToHistory(data)
if (Settings.data.notifications?.doNotDisturb)
if (Settings.data.notifications?.doNotDisturb || PowerProfileService.noctaliaPerformanceMode)
return
activeMap[data.id] = notification
+23
View File
@@ -13,6 +13,9 @@ Singleton {
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
// Not a power profile but a volatile property to quickly disable shadows, animations, etc..
property bool noctaliaPerformanceMode: false
function getName(p) {
if (!available)
return "Unknown"
@@ -94,4 +97,24 @@ Singleton {
}
}
}
// Noctalia Performance Mode
// - Turning shadow off
// - Turning animation off
// - Do Not Disturb
function toggleNoctaliaPerformance() {
noctaliaPerformanceMode = !noctaliaPerformanceMode
}
function setNoctaliaPerformance(value) {
noctaliaPerformanceMode = value
}
onNoctaliaPerformanceModeChanged: {
if (noctaliaPerformanceMode) {
ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.enabled"), "seedling")
} else {
ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.disabled"), "seedling-off")
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ Singleton {
id: root
// Public properties
property string baseVersion: "3.0.0"
property string baseVersion: "3.0.2"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`
+35 -25
View File
@@ -14,40 +14,50 @@ Item {
implicitWidth: size
implicitHeight: size
Canvas {
id: canvas
// GPU-optimized spinner - draw once, rotate with GPU transform
Item {
id: spinner
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.reset()
// Static canvas - drawn ONCE, then cached
Canvas {
id: canvas
anchors.fill: parent
renderStrategy: Canvas.Cooperative // Better performance than Threaded for simple shapes
renderTarget: Canvas.FramebufferObject // GPU texture
var centerX = width / 2
var centerY = height / 2
var radius = Math.min(width, height) / 2 - strokeWidth / 2
// Enable layer caching - critical for performance!
layer.enabled: true
layer.smooth: true
ctx.strokeStyle = root.color
ctx.lineWidth = Math.max(1, root.strokeWidth)
ctx.lineCap = "round"
Component.onCompleted: {
requestPaint()
}
// Draw arc with gap (270 degrees with 90 degree gap)
ctx.beginPath()
ctx.arc(centerX, centerY, radius, -Math.PI / 2 + rotationAngle, -Math.PI / 2 + rotationAngle + Math.PI * 1.5)
ctx.stroke()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var centerX = width / 2
var centerY = height / 2
var radius = Math.min(width, height) / 2 - strokeWidth / 2
ctx.strokeStyle = root.color
ctx.lineWidth = Math.max(1, root.strokeWidth)
ctx.lineCap = "round"
// Draw arc with gap (270 degrees = 3/4 of circle)
ctx.beginPath()
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + Math.PI * 1.5)
ctx.stroke()
}
}
property real rotationAngle: 0
onRotationAngleChanged: {
requestPaint()
}
NumberAnimation {
target: canvas
property: "rotationAngle"
// Smooth rotation animation - uses GPU transform, NO canvas repaints!
RotationAnimation on rotation {
running: root.running
from: 0
to: 2 * Math.PI
to: 360
duration: root.duration
loops: Animation.Infinite
}
+35 -5
View File
@@ -24,8 +24,27 @@ Rectangle {
border.color: flat ? Color.transparent : Color.mSurfaceVariant
border.width: flat ? 0 : Style.borderS
// Repaint gauge when the bound value changes
onValueChanged: gauge.requestPaint()
// Animated value for smooth transitions - reduces repaint frequency
property real animatedValue: value
Behavior on animatedValue {
enabled: !Settings.data.general.animationDisabled
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
// Repaint gauge when animated value changes (throttled by animation)
onAnimatedValueChanged: repaintTimer.restart()
// Debounce timer to limit repaint frequency during rapid value changes
Timer {
id: repaintTimer
interval: 16 // ~60 FPS max
repeat: false
onTriggered: gauge.requestPaint()
}
ColumnLayout {
id: mainLayout
@@ -45,7 +64,18 @@ Rectangle {
Canvas {
id: gauge
anchors.fill: parent
renderStrategy: Canvas.Immediate
// Optimized Canvas settings for better GPU performance
renderStrategy: Canvas.Cooperative // Better performance than Immediate
renderTarget: Canvas.FramebufferObject // GPU texture rendering
// Enable layer caching - critical for performance!
layer.enabled: true
layer.smooth: true
Component.onCompleted: {
requestPaint()
}
onPaint: {
const ctx = getContext("2d")
@@ -68,7 +98,7 @@ Rectangle {
ctx.stroke()
// Value arc with gradient starting at 25%
const ratio = Math.max(0, Math.min(1, root.value / 100))
const ratio = Math.max(0, Math.min(1, root.animatedValue / 100))
const end = start + (endBg - start) * ratio
// Calculate gradient start point (25% into the arc)
@@ -97,7 +127,7 @@ Rectangle {
id: valueLabel
anchors.centerIn: parent
anchors.verticalCenterOffset: -4 * contentScale
text: `${root.value}${root.suffix}`
text: `${Math.round(root.value)}${root.suffix}`
pointSize: Style.fontSizeM * contentScale * 0.9
font.weight: Style.fontWeightBold
color: Color.mOnSurface
+2 -1
View File
@@ -1,13 +1,14 @@
import QtQuick
import QtQuick.Effects
import qs.Commons
import qs.Services
// Unified shadow system
Item {
id: root
required property var source
layer.enabled: Settings.data.general.enableShadows
layer.enabled: Settings.data.general.enableShadows && !PowerProfileService.noctaliaPerformanceMode
layer.effect: MultiEffect {
source: root.source
shadowEnabled: true
+7 -46
View File
@@ -13,7 +13,13 @@ Item {
property real handleWidth: 6
property real handleRadius: Style.radiusM
property int verticalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AsNeeded
property int horizontalPolicy: ScrollBar.AlwaysOff
readonly property bool verticalScrollBarActive: {
if (listView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff)
return false
return listView.contentHeight > listView.height
}
readonly property real scrollBarWidth: verticalScrollBarActive ? handleWidth : 0
// Forward ListView properties
property alias model: listView.model
@@ -118,7 +124,6 @@ Item {
x: listView.mirrored ? 0 : listView.width - width
y: 0
height: listView.height
active: listView.ScrollBar.horizontal.active
policy: root.verticalPolicy
contentItem: Rectangle {
@@ -155,49 +160,5 @@ Item {
}
}
}
ScrollBar.horizontal: ScrollBar {
id: horizontalScrollBar
parent: listView
x: 0
y: listView.height - height
width: listView.width
active: listView.ScrollBar.vertical.active
policy: root.horizontalPolicy
contentItem: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
}
-209
View File
@@ -1,209 +0,0 @@
import QtQuick
import QtQuick.Effects
import qs.Commons
Item {
id: root
// Corner radius properties
property real topLeftRadius: 0
property real topRightRadius: 0
property real bottomLeftRadius: 0
property real bottomRightRadius: 0
// Inverted corner properties (concave instead of convex)
property bool topLeftInverted: false
property bool topRightInverted: false
property bool bottomLeftInverted: false
property bool bottomRightInverted: false
// Direction for inverted corners: "horizontal" or "vertical"
// horizontal: curves left/right (extends beyond left/right edges)
// vertical: curves up/down (extends beyond top/bottom edges)
property string topLeftInvertedDirection: "horizontal" // default: curves left
property string topRightInvertedDirection: "horizontal" // default: curves right
property string bottomLeftInvertedDirection: "horizontal" // default: curves left
property string bottomRightInvertedDirection: "horizontal" // default: curves right
// Background color and borders
property color backgroundColor: Color.mPrimary
property color borderColor: Color.mOutline
property int borderWidth: Style.borderS
// Check if any corner is inverted
readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted
// Calculate padding needed for inverted corners based on their direction
readonly property real topPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "vertical") ? topLeftRadius : 0, (topRightInverted && topRightInvertedDirection === "vertical") ? topRightRadius : 0)
readonly property real bottomPadding: Math.max((bottomLeftInverted && bottomLeftInvertedDirection === "vertical") ? bottomLeftRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "vertical") ? bottomRightRadius : 0)
readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0)
readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0)
// Background layer
Item {
id: backgroundLayer
anchors.fill: parent
z: 0
// Simple rectangle for non-inverted corners (better performance)
Rectangle {
id: simpleBackground
anchors.fill: parent
color: root.backgroundColor
radius: topLeftRadius // Use topLeftRadius as default
border.width: borderWidth
border.color: borderColor
visible: !root.hasInvertedCorners
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
// Background with custom corners (for inverted corners)
Canvas {
id: background
anchors.fill: parent
anchors.topMargin: -root.topPadding
anchors.bottomMargin: -root.bottomPadding
anchors.leftMargin: -root.leftPadding
anchors.rightMargin: -root.rightPadding
visible: root.hasInvertedCorners
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
// Adjust coordinates to account for inverted corner padding
var x = root.leftPadding
var y = root.topPadding
var w = width - root.leftPadding - root.rightPadding
var h = height - root.topPadding - root.bottomPadding
ctx.fillStyle = root.backgroundColor
ctx.beginPath()
// Start from top left
if (topLeftInverted) {
if (topLeftInvertedDirection === "vertical") {
ctx.moveTo(x, y)
} else {
ctx.moveTo(x + topLeftRadius, y)
}
} else {
ctx.moveTo(x + topLeftRadius, y)
}
// Top edge and top right corner
if (topRightInverted) {
if (topRightInvertedDirection === "horizontal") {
// Curves to the right
ctx.lineTo(x + w, y)
ctx.lineTo(x + w + topRightRadius, y)
ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius)
} else {
// Curves upward
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y - topRightRadius)
ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y)
ctx.lineTo(x + w, y)
ctx.lineTo(x + w, y + topRightRadius)
}
} else {
ctx.lineTo(x + w - topRightRadius, y)
ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
}
// Right edge and bottom right corner
if (bottomRightInverted) {
if (bottomRightInvertedDirection === "horizontal") {
// Curves to the right
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h)
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w - bottomRightRadius, y + h)
} else {
// Curves downward
ctx.lineTo(x + w, y + h)
ctx.lineTo(x + w, y + h + bottomRightRadius)
ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h)
}
} else {
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius)
}
// Bottom edge and bottom left corner
if (bottomLeftInverted) {
if (bottomLeftInvertedDirection === "horizontal") {
// Curves to the left
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.lineTo(x - bottomLeftRadius, y + h)
ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius)
} else {
// Curves downward
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.quadraticCurveTo(x, y + h, x, y + h + bottomLeftRadius)
ctx.lineTo(x, y + h + bottomLeftRadius)
}
} else {
ctx.lineTo(x + bottomLeftRadius, y + h)
ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius)
}
// Left edge and back to top left corner
if (topLeftInverted) {
if (topLeftInvertedDirection === "horizontal") {
// Curves to the left
ctx.lineTo(x, y + topLeftRadius)
ctx.quadraticCurveTo(x, y, x - topLeftRadius, y)
ctx.lineTo(x, y)
ctx.lineTo(x + topLeftRadius, y)
} else {
// Curves upward
ctx.lineTo(x, y + topLeftRadius)
ctx.lineTo(x, y)
ctx.lineTo(x, y - topLeftRadius)
ctx.quadraticCurveTo(x, y, x + topLeftRadius, y)
}
} else {
ctx.lineTo(x, y + topLeftRadius)
ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
}
ctx.closePath()
ctx.fill()
}
}
}
// Content layer: for child elements
default property alias contentChildren: contentLayer.data
Item {
id: contentLayer
anchors.fill: parent
z: 1
}
// Trigger repaint when properties change
onTopLeftRadiusChanged: background.requestPaint()
onTopRightRadiusChanged: background.requestPaint()
onBottomLeftRadiusChanged: background.requestPaint()
onBottomRightRadiusChanged: background.requestPaint()
onTopLeftInvertedChanged: background.requestPaint()
onTopRightInvertedChanged: background.requestPaint()
onBottomLeftInvertedChanged: background.requestPaint()
onBottomRightInvertedChanged: background.requestPaint()
onTopLeftInvertedDirectionChanged: background.requestPaint()
onTopRightInvertedDirectionChanged: background.requestPaint()
onBottomLeftInvertedDirectionChanged: background.requestPaint()
onBottomRightInvertedDirectionChanged: background.requestPaint()
onBackgroundColorChanged: background.requestPaint()
onWidthChanged: background.requestPaint()
onHeightChanged: background.requestPaint()
}
+5 -28
View File
@@ -7,6 +7,7 @@ import qs.Services
Slider {
id: root
property color fillColor: Color.mPrimary
property var cutoutColor: Color.mSurface
property bool snapAlways: true
property real heightRatio: 0.7
@@ -46,7 +47,7 @@ Slider {
width: parent.height
height: parent.height
radius: width / 2
color: Qt.darker(Color.mPrimary, 1.2) //starting color of gradient
color: Qt.darker(fillColor, 1.2) //starting color of gradient
}
// The main rectangle
@@ -60,35 +61,11 @@ Slider {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Qt.darker(Color.mPrimary, 1.2)
Behavior on color {
ColorAnimation {
duration: 300
}
}
}
GradientStop {
position: 0.5
color: Color.mPrimary
SequentialAnimation on position {
loops: Animation.Infinite
NumberAnimation {
from: 0.3
to: 0.7
duration: 2000
easing.type: Easing.InOutSine
}
NumberAnimation {
from: 0.7
to: 0.3
duration: 2000
easing.type: Easing.InOutSine
}
}
color: Qt.darker(fillColor, 1.2)
}
GradientStop {
position: 1.0
color: Qt.lighter(Color.mPrimary, 1.2)
color: fillColor
}
}
}
@@ -118,7 +95,7 @@ Slider {
implicitHeight: knobDiameter
radius: width / 2
color: root.pressed ? Color.mHover : Color.mSurface
border.color: Color.mPrimary
border.color: fillColor
border.width: Style.borderL
anchors.centerIn: parent
+23 -27
View File
@@ -74,6 +74,11 @@ ShellRoot {
IdleInhibitorService.init()
PowerProfileService.init()
DistroService.init()
// Only open the setup wizard for new users
if (!Settings.data.setupCompleted) {
checkSetupWizard()
}
}
Overview {}
@@ -96,18 +101,7 @@ ShellRoot {
IPCService {}
// MainScreen for each screen (manages bar + all panels)
MainScreens {}
}
}
// Setup Wizard - Auto Kick start
Connections {
target: Settings
function onSettingsLoaded() {
// Only open the setup wizard for new users
if (!Settings.data.setupCompleted) {
checkSetupWizard()
}
AllScreens {}
}
}
@@ -125,23 +119,25 @@ ShellRoot {
}
if (Settings.data.settingsVersion >= Settings.settingsVersion) {
// Open Setup Wizard as a panel in the same windowing system as Settings/ControlCenter
if (Quickshell.screens.length > 0) {
var targetScreen = Quickshell.screens[0]
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen)
if (setupPanel) {
setupPanel.open()
} else {
// If not yet loaded, ensure it loads and try again shortly
Qt.callLater(() => {
var sp = PanelService.getPanel("setupWizardPanel", targetScreen)
if (sp)
sp.open()
})
}
}
showSetupWizard()
} else {
Settings.data.setupCompleted = true
}
}
function showSetupWizard() {
// Open Setup Wizard as a panel in the same windowing system as Settings/ControlCenter
if (Quickshell.screens.length > 0) {
var targetScreen = Quickshell.screens[0]
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen)
if (setupPanel) {
setupPanel.open()
} else {
// If not yet loaded, ensure it loads and try again shortly
Qt.callLater(() => {
showSetupWizard()
})
}
}
}
}