From 430614a26a10768e224d8af4810d04e8f0088cf3 Mon Sep 17 00:00:00 2001 From: AleksElixir <71710534+AleksElixir@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:50:02 +0200 Subject: [PATCH 1/4] Add files via upload --- .github/assets/bongocat.gif | Bin 0 -> 7350 bytes .github/assets/dino.png | Bin 0 -> 9441 bytes .github/assets/kurukuru.gif | Bin 0 -> 99006 bytes .github/assets/logo.svg | 79 +++ .github/assets/pam.d/fprint | 3 + .github/assets/pam.d/passwd | 6 + .github/assets/shaders/opacitymask.frag | 19 + .github/assets/shaders/opacitymask.frag.qsb | Bin 0 -> 1337 bytes .github/assets/wrap_term_launch.sh | 5 + .github/components/Anim.qml | 8 + .github/components/CAnim.qml | 8 + .github/components/CategoryNavbar.qml | 230 +++++++++ .github/components/ConnectionHeader.qml | 31 ++ .github/components/ConnectionInfoSection.qml | 59 +++ .github/components/Logo.qml | 70 +++ .github/components/MaterialIcon.qml | 16 + .github/components/PropertyRow.qml | 26 + .github/components/SectionContainer.qml | 32 ++ .github/components/SectionHeader.qml | 27 + .github/components/StateLayer.qml | 95 ++++ .github/components/StyledClippingRect.qml | 12 + .github/components/StyledRect.qml | 11 + .github/components/StyledText.qml | 48 ++ .../components/containers/StyledFlickable.qml | 14 + .../components/containers/StyledListView.qml | 14 + .../components/containers/StyledWindow.qml | 9 + .../components/controls/CircularIndicator.qml | 108 ++++ .../components/controls/CircularProgress.qml | 69 +++ .../controls/CollapsibleSection.qml | 132 +++++ .../components/controls/CustomMouseArea.qml | 21 + .github/components/controls/CustomSpinBox.qml | 170 +++++++ .github/components/controls/FilledSlider.qml | 146 ++++++ .github/components/controls/IconButton.qml | 83 +++ .../components/controls/IconTextButton.qml | 88 ++++ .github/components/controls/Menu.qml | 113 +++++ .github/components/controls/MenuItem.qml | 11 + .github/components/controls/SpinBoxRow.qml | 52 ++ .github/components/controls/SplitButton.qml | 164 ++++++ .../components/controls/SplitButtonRow.qml | 62 +++ .../components/controls/StyledInputField.qml | 79 +++ .../components/controls/StyledRadioButton.qml | 57 +++ .../components/controls/StyledScrollBar.qml | 190 +++++++ .github/components/controls/StyledSlider.qml | 57 +++ .github/components/controls/StyledSwitch.qml | 152 ++++++ .../components/controls/StyledTextField.qml | 76 +++ .github/components/controls/SwitchRow.qml | 48 ++ .github/components/controls/TextButton.qml | 78 +++ .github/components/controls/ToggleButton.qml | 124 +++++ .github/components/controls/ToggleRow.qml | 28 ++ .github/components/controls/Tooltip.qml | 185 +++++++ .github/components/effects/ColouredIcon.qml | 35 ++ .github/components/effects/Colouriser.qml | 14 + .github/components/effects/Elevation.qml | 18 + .github/components/effects/InnerBorder.qml | 44 ++ .github/components/effects/OpacityMask.qml | 9 + .github/components/filedialog/CurrentItem.qml | 102 ++++ .../components/filedialog/DialogButtons.qml | 93 ++++ .github/components/filedialog/FileDialog.qml | 102 ++++ .../components/filedialog/FolderContents.qml | 228 +++++++++ .github/components/filedialog/HeaderBar.qml | 139 +++++ .github/components/filedialog/Sidebar.qml | 113 +++++ .github/components/filedialog/Sizes.qml | 8 + .../components/images/CachingIconImage.qml | 42 ++ .github/components/images/CachingImage.qml | 28 ++ .github/components/misc/CustomShortcut.qml | 5 + .github/components/misc/Ref.qml | 8 + .github/components/widgets/ExtraIndicator.qml | 51 ++ .github/config/Appearance.qml | 14 + .github/config/AppearanceConfig.qml | 94 ++++ .github/config/BackgroundConfig.qml | 37 ++ .github/config/BarConfig.qml | 127 +++++ .github/config/BorderConfig.qml | 6 + .github/config/Config.qml | 476 ++++++++++++++++++ .github/config/ControlCenterConfig.qml | 10 + .github/config/DashboardConfig.qml | 40 ++ .github/config/GeneralConfig.qml | 61 +++ .github/config/LauncherConfig.qml | 159 ++++++ .github/config/LockConfig.qml | 15 + .github/config/NotifsConfig.qml | 18 + .github/config/OsdConfig.qml | 14 + .github/config/ServiceConfig.qml | 22 + .github/config/SessionConfig.qml | 29 ++ .github/config/SidebarConfig.qml | 11 + .github/config/UserPaths.qml | 8 + .github/config/UtilitiesConfig.qml | 66 +++ .github/config/WInfoConfig.qml | 10 + .github/extras/CMakeLists.txt | 9 + .github/extras/version.cpp | 27 + 88 files changed, 5407 insertions(+) create mode 100644 .github/assets/bongocat.gif create mode 100644 .github/assets/dino.png create mode 100644 .github/assets/kurukuru.gif create mode 100644 .github/assets/logo.svg create mode 100644 .github/assets/pam.d/fprint create mode 100644 .github/assets/pam.d/passwd create mode 100644 .github/assets/shaders/opacitymask.frag create mode 100644 .github/assets/shaders/opacitymask.frag.qsb create mode 100644 .github/assets/wrap_term_launch.sh create mode 100644 .github/components/Anim.qml create mode 100644 .github/components/CAnim.qml create mode 100644 .github/components/CategoryNavbar.qml create mode 100644 .github/components/ConnectionHeader.qml create mode 100644 .github/components/ConnectionInfoSection.qml create mode 100644 .github/components/Logo.qml create mode 100644 .github/components/MaterialIcon.qml create mode 100644 .github/components/PropertyRow.qml create mode 100644 .github/components/SectionContainer.qml create mode 100644 .github/components/SectionHeader.qml create mode 100644 .github/components/StateLayer.qml create mode 100644 .github/components/StyledClippingRect.qml create mode 100644 .github/components/StyledRect.qml create mode 100644 .github/components/StyledText.qml create mode 100644 .github/components/containers/StyledFlickable.qml create mode 100644 .github/components/containers/StyledListView.qml create mode 100644 .github/components/containers/StyledWindow.qml create mode 100644 .github/components/controls/CircularIndicator.qml create mode 100644 .github/components/controls/CircularProgress.qml create mode 100644 .github/components/controls/CollapsibleSection.qml create mode 100644 .github/components/controls/CustomMouseArea.qml create mode 100644 .github/components/controls/CustomSpinBox.qml create mode 100644 .github/components/controls/FilledSlider.qml create mode 100644 .github/components/controls/IconButton.qml create mode 100644 .github/components/controls/IconTextButton.qml create mode 100644 .github/components/controls/Menu.qml create mode 100644 .github/components/controls/MenuItem.qml create mode 100644 .github/components/controls/SpinBoxRow.qml create mode 100644 .github/components/controls/SplitButton.qml create mode 100644 .github/components/controls/SplitButtonRow.qml create mode 100644 .github/components/controls/StyledInputField.qml create mode 100644 .github/components/controls/StyledRadioButton.qml create mode 100644 .github/components/controls/StyledScrollBar.qml create mode 100644 .github/components/controls/StyledSlider.qml create mode 100644 .github/components/controls/StyledSwitch.qml create mode 100644 .github/components/controls/StyledTextField.qml create mode 100644 .github/components/controls/SwitchRow.qml create mode 100644 .github/components/controls/TextButton.qml create mode 100644 .github/components/controls/ToggleButton.qml create mode 100644 .github/components/controls/ToggleRow.qml create mode 100644 .github/components/controls/Tooltip.qml create mode 100644 .github/components/effects/ColouredIcon.qml create mode 100644 .github/components/effects/Colouriser.qml create mode 100644 .github/components/effects/Elevation.qml create mode 100644 .github/components/effects/InnerBorder.qml create mode 100644 .github/components/effects/OpacityMask.qml create mode 100644 .github/components/filedialog/CurrentItem.qml create mode 100644 .github/components/filedialog/DialogButtons.qml create mode 100644 .github/components/filedialog/FileDialog.qml create mode 100644 .github/components/filedialog/FolderContents.qml create mode 100644 .github/components/filedialog/HeaderBar.qml create mode 100644 .github/components/filedialog/Sidebar.qml create mode 100644 .github/components/filedialog/Sizes.qml create mode 100644 .github/components/images/CachingIconImage.qml create mode 100644 .github/components/images/CachingImage.qml create mode 100644 .github/components/misc/CustomShortcut.qml create mode 100644 .github/components/misc/Ref.qml create mode 100644 .github/components/widgets/ExtraIndicator.qml create mode 100644 .github/config/Appearance.qml create mode 100644 .github/config/AppearanceConfig.qml create mode 100644 .github/config/BackgroundConfig.qml create mode 100644 .github/config/BarConfig.qml create mode 100644 .github/config/BorderConfig.qml create mode 100644 .github/config/Config.qml create mode 100644 .github/config/ControlCenterConfig.qml create mode 100644 .github/config/DashboardConfig.qml create mode 100644 .github/config/GeneralConfig.qml create mode 100644 .github/config/LauncherConfig.qml create mode 100644 .github/config/LockConfig.qml create mode 100644 .github/config/NotifsConfig.qml create mode 100644 .github/config/OsdConfig.qml create mode 100644 .github/config/ServiceConfig.qml create mode 100644 .github/config/SessionConfig.qml create mode 100644 .github/config/SidebarConfig.qml create mode 100644 .github/config/UserPaths.qml create mode 100644 .github/config/UtilitiesConfig.qml create mode 100644 .github/config/WInfoConfig.qml create mode 100644 .github/extras/CMakeLists.txt create mode 100644 .github/extras/version.cpp diff --git a/.github/assets/bongocat.gif b/.github/assets/bongocat.gif new file mode 100644 index 0000000000000000000000000000000000000000..f960fecd7d91c028506ddf7a7ace5bcb84c473bd GIT binary patch literal 7350 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 literal 0 HcmV?d00001 diff --git a/.github/assets/dino.png b/.github/assets/dino.png new file mode 100644 index 0000000000000000000000000000000000000000..b5bc7bb3bb76e96fec17939f8256f898ff330786 GIT binary patch literal 9441 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 KVd#zwz5fGU95{Ob literal 0 HcmV?d00001 diff --git a/.github/assets/logo.svg b/.github/assets/logo.svg new file mode 100644 index 000000000..6879c92b9 --- /dev/null +++ b/.github/assets/logo.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + diff --git a/.github/assets/pam.d/fprint b/.github/assets/pam.d/fprint new file mode 100644 index 000000000..d4814e946 --- /dev/null +++ b/.github/assets/pam.d/fprint @@ -0,0 +1,3 @@ +#%PAM-1.0 + +auth required pam_fprintd.so max-tries=1 diff --git a/.github/assets/pam.d/passwd b/.github/assets/pam.d/passwd new file mode 100644 index 000000000..4b1406432 --- /dev/null +++ b/.github/assets/pam.d/passwd @@ -0,0 +1,6 @@ +#%PAM-1.0 + +auth required pam_faillock.so preauth +auth [success=1 default=bad] pam_unix.so nullok +auth [default=die] pam_faillock.so authfail +auth required pam_faillock.so authsucc diff --git a/.github/assets/shaders/opacitymask.frag b/.github/assets/shaders/opacitymask.frag new file mode 100644 index 000000000..94a80b8a9 --- /dev/null +++ b/.github/assets/shaders/opacitymask.frag @@ -0,0 +1,19 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + // qt_Matrix and qt_Opacity must always be both present + // if the built-in vertex shader is used. + mat4 qt_Matrix; + float qt_Opacity; +}; + +layout(binding = 1) uniform sampler2D source; +layout(binding = 2) uniform sampler2D maskSource; + +void main() +{ + fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity; +} diff --git a/.github/assets/shaders/opacitymask.frag.qsb b/.github/assets/shaders/opacitymask.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..7bf97c280f5b98e09f7e83c6dc2c678d7901e9ec GIT binary patch literal 1337 zcmV-91;+XS01%LPoZVM#Pa8)N9@`KXLtdaHAx%gwp^fYoxyEs-s1Aq<(l$~e+8Cvk zEOa`~XLG^6bME$tQN&;9$Nq+X>Hq7Os-4-L^R3S|L8>ZMJ#_eH_L*m9pPk*a0bmXQ zX7JA7y#jARfC>BHzyNl@$G-qtP{FY*-tZS+FySJCC1_)S1sxgOmEns1gQ1t;5F!kx zClemQ20ra-n5?8%^}WOt0;yfB5@4PY+Zy^>{IJX+d(g)q8$$$~a?0(vwrLmTT!0X# z5n(nPRRJjSBr=l%mZ67PLO6!T#GveX*)Dor;?Gv%%KethuH~tydH4t(vUd^lQJM1? zLZ9EgXuK$gqE)UvrWJ)NkdrtXy9#-!1>HT%^~gUbIY`W$^yi5KpU8igbC7)*KkqY7 zY(LP(7gP9U#%DD z9M4Hr==ladk=O>_HxZv_@n@uO29*YhC4ZgmIriPcrvl^CtrFX}5lPKkV7>+R&odv{ zm)S@8uQFej@z)?;JB25Ek$rLeGRI#>dxhsh*B5Nx;(AxmzQc2&`Q2sh?{H4acb9Xo zGC$e(*hl;Z^JkdffTS-d{sCi&?<-uREF}AnYWM>^w9cF8B@M3d{5N^-OFX}y5S5o} zrZ7ud%yph~9CITTbCWUT{}Fvu%O~uo8h&BiZRV%3Z_r1zukn2EGe5w!A`bN9!TcSSZc0F_! zLJ>r^Fb<=3;eEltt7nOt@m82GEfKhby7Wl~a_;pl+ZCtv!Wo7}p4;|=9>p5wqYcfY z5`D3u6IFkhcTM2A5!(7 zX-darHPJLDY}BCb1X7Bxjlk)+p>TrYVI{6H@ZXs|>kZZ@wX6+ll)@b9j8T~C*l)K( zM-=T!Nf(}xTucgDE2-7kkK5&`%CN=S>-7lYZKx$^2unDIsX!*Liw+J6JkFX3H^RIz z#+(2rvQq$dpCw$IHPIJe9~f%^?%Bo<&tJVZcLF~Q&F5Y}5^+C_2Vv&@XFu=k?Y-Kq zD7hwZ>_s9)qLqXUjo0SB6?7cI6tWWuU`}sGC2v_NQ%tEn-kphN!;ojSxW9Be@^MR2C? zNBYXLknG3o?`y7SccYeLJncEc>YDf!yXziq6%v2w_F7-kzw*%*9$X=v<(}1ZLKM(3 zWYna$_;B}L=!E2OsFj1R3UNPmecMaIiMr=_T59!F zj0y2u3P#4OdT_9A*uICRCnx)m+6l+isV2)>AHI)T>L?hVFU@9rrcS|WCq{zZkl6TO zP*e>xNDTHeUyMm;(m3KO4PRgJgz4Ti-k+%j@>4w)?@Yx})CyQfZOZ3tOpZvEGafbh vMWQvdulkr8c~+LqZ/dev/null + +exec "$@" diff --git a/.github/components/Anim.qml b/.github/components/Anim.qml new file mode 100644 index 000000000..6883a7984 --- /dev/null +++ b/.github/components/Anim.qml @@ -0,0 +1,8 @@ +import qs.config +import QtQuick + +NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard +} diff --git a/.github/components/CAnim.qml b/.github/components/CAnim.qml new file mode 100644 index 000000000..49484b789 --- /dev/null +++ b/.github/components/CAnim.qml @@ -0,0 +1,8 @@ +import qs.config +import QtQuick + +ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard +} diff --git a/.github/components/CategoryNavbar.qml b/.github/components/CategoryNavbar.qml new file mode 100644 index 000000000..a3e813b5b --- /dev/null +++ b/.github/components/CategoryNavbar.qml @@ -0,0 +1,230 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var categories + required property string activeCategory + property bool showScrollButtons: true + property bool showExtraContent: false + property Component extraContent: null + + signal categoryChanged(string categoryId) + + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.normal + + visible: opacity > 0 + implicitHeight: tabsRow.height + Appearance.padding.small + Appearance.padding.normal + clip: true + + function scrollToActiveTab(): void { + Qt.callLater(() => { + if (!tabsFlickable || !tabsRow) + return; + + const currentIndex = root.categories.findIndex(cat => cat.id === root.activeCategory); + if (currentIndex === -1) + return; + + let tabX = 0; + for (let i = 0; i < currentIndex && i < tabsRow.children.length; i++) { + const child = tabsRow.children[i]; + if (child) { + tabX += child.width + tabsRow.spacing; + } + } + + const activeTab = tabsRow.children[currentIndex]; + if (!activeTab) + return; + + const tabWidth = activeTab.width; + const viewportStart = tabsFlickable.contentX; + const viewportEnd = tabsFlickable.contentX + tabsFlickable.width; + + if (tabX < viewportStart) { + tabsFlickable.contentX = tabX; + } else if (tabX + tabWidth > viewportEnd) { + tabsFlickable.contentX = Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabX + tabWidth - tabsFlickable.width); + } + }); + } + + RowLayout { + id: tabsContent + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.small + anchors.bottomMargin: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + + IconButton { + icon: "chevron_left" + visible: root.showScrollButtons && tabsFlickable.contentWidth > tabsFlickable.width + type: IconButton.Text + radius: Appearance.rounding.small + padding: Appearance.padding.small + onClicked: { + tabsFlickable.contentX = Math.max(0, tabsFlickable.contentX - 100); + } + } + + StyledFlickable { + id: tabsFlickable + Layout.fillWidth: true + Layout.preferredHeight: tabsRow.height + flickableDirection: Flickable.HorizontalFlick + contentWidth: tabsRow.width + clip: true + + Behavior on contentX { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + + onWheel: wheel => { + const delta = wheel.angleDelta.y || wheel.angleDelta.x; + tabsFlickable.contentX = Math.max(0, Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabsFlickable.contentX - delta)); + wheel.accepted = true; + } + + onPressed: mouse => { + mouse.accepted = false; + } + } + + Item { + implicitWidth: tabsRow.width + implicitHeight: tabsRow.height + + StyledRect { + id: activeIndicator + + property Item activeTab: { + for (let i = 0; i < tabsRepeater.count; i++) { + const tab = tabsRepeater.itemAt(i); + if (tab && tab.isActive) { + return tab; + } + } + return null; + } + + visible: activeTab !== null + color: Colours.palette.m3primary + radius: 10 + + x: activeTab ? activeTab.x : 0 + y: activeTab ? activeTab.y : 0 + width: activeTab ? activeTab.width : 0 + height: activeTab ? activeTab.height : 0 + + Behavior on x { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + Behavior on width { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + + Row { + id: tabsRow + spacing: Appearance.spacing.small + + Repeater { + id: tabsRepeater + model: root.categories + + delegate: Item { + required property var modelData + required property int index + + property bool isActive: root.activeCategory === modelData.id + + implicitWidth: tabContent.width + Appearance.padding.normal * 2 + implicitHeight: tabContent.height + Appearance.padding.smaller * 2 + + StateLayer { + anchors.fill: parent + radius: 6 + function onClicked(): void { + root.categoryChanged(modelData.id); + + const tabLeft = parent.x; + const tabRight = parent.x + parent.width; + const viewLeft = tabsFlickable.contentX; + const viewRight = tabsFlickable.contentX + tabsFlickable.width; + + const targetX = tabLeft - (tabsFlickable.width - parent.width) / 2; + + tabsFlickable.contentX = Math.max(0, Math.min(tabsFlickable.contentWidth - tabsFlickable.width, targetX)); + } + } + + Row { + id: tabContent + anchors.centerIn: parent + spacing: Appearance.spacing.smaller + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3surface : Colours.palette.m3onSurfaceVariant + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3surface : Colours.palette.m3onSurfaceVariant + } + } + } + } + } + } + } + + IconButton { + icon: "chevron_right" + visible: root.showScrollButtons && tabsFlickable.contentWidth > tabsFlickable.width + type: IconButton.Text + radius: Appearance.rounding.small + padding: Appearance.padding.small + onClicked: { + tabsFlickable.contentX = Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabsFlickable.contentX + 100); + } + } + + Loader { + Layout.fillHeight: true + active: root.showExtraContent && root.extraContent !== null + sourceComponent: root.extraContent + } + } +} diff --git a/.github/components/ConnectionHeader.qml b/.github/components/ConnectionHeader.qml new file mode 100644 index 000000000..12b427648 --- /dev/null +++ b/.github/components/ConnectionHeader.qml @@ -0,0 +1,31 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string icon + required property string title + + spacing: Appearance.spacing.normal + Layout.alignment: Qt.AlignHCenter + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.title + font.pointSize: Appearance.font.size.large + font.bold: true + } +} diff --git a/.github/components/ConnectionInfoSection.qml b/.github/components/ConnectionInfoSection.qml new file mode 100644 index 000000000..927ef287d --- /dev/null +++ b/.github/components/ConnectionInfoSection.qml @@ -0,0 +1,59 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property var deviceDetails + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: root.deviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: root.deviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: root.deviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } +} diff --git a/.github/components/Logo.qml b/.github/components/Logo.qml new file mode 100644 index 000000000..7cd41e17f --- /dev/null +++ b/.github/components/Logo.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Shapes +import qs.services + +Item { + id: root + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + implicitWidth: designWidth + implicitHeight: designHeight + + Shape { + anchors.centerIn: parent + width: root.designWidth + height: root.designHeight + scale: Math.min(root.width / width, root.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + + ShapePath { + fillColor: root.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } +} diff --git a/.github/components/MaterialIcon.qml b/.github/components/MaterialIcon.qml new file mode 100644 index 000000000..a1d19d3c0 --- /dev/null +++ b/.github/components/MaterialIcon.qml @@ -0,0 +1,16 @@ +import qs.services +import qs.config + +StyledText { + property real fill + property int grade: Colours.light ? 0 : -25 + + font.family: Appearance.font.family.material + font.pointSize: Appearance.font.size.larger + font.variableAxes: ({ + FILL: fill.toFixed(1), + GRAD: grade, + opsz: fontInfo.pixelSize, + wght: fontInfo.weight + }) +} diff --git a/.github/components/PropertyRow.qml b/.github/components/PropertyRow.qml new file mode 100644 index 000000000..640d5f743 --- /dev/null +++ b/.github/components/PropertyRow.qml @@ -0,0 +1,26 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string label + required property string value + property bool showTopMargin: false + + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 + text: root.label + } + + StyledText { + text: root.value + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } +} diff --git a/.github/components/SectionContainer.qml b/.github/components/SectionContainer.qml new file mode 100644 index 000000000..2b653a5d9 --- /dev/null +++ b/.github/components/SectionContainer.qml @@ -0,0 +1,32 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + default property alias content: contentColumn.data + property real contentSpacing: Appearance.spacing.larger + property bool alignTop: false + + Layout.fillWidth: true + implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh + + ColumnLayout { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: root.alignTop ? parent.top : undefined + anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: root.contentSpacing + } +} diff --git a/.github/components/SectionHeader.qml b/.github/components/SectionHeader.qml new file mode 100644 index 000000000..502e91895 --- /dev/null +++ b/.github/components/SectionHeader.qml @@ -0,0 +1,27 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + + spacing: 0 + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + visible: root.description !== "" + text: root.description + color: Colours.palette.m3outline + } +} diff --git a/.github/components/StateLayer.qml b/.github/components/StateLayer.qml new file mode 100644 index 000000000..a20e26616 --- /dev/null +++ b/.github/components/StateLayer.qml @@ -0,0 +1,95 @@ +import qs.services +import qs.config +import QtQuick + +MouseArea { + id: root + + property bool disabled + property bool showHoverBackground: true + property color color: Colours.palette.m3onSurface + property real radius: parent?.radius ?? 0 + property alias rect: hoverLayer + + function onClicked(): void { + } + + anchors.fill: parent + + enabled: !disabled + cursorShape: disabled ? undefined : Qt.PointingHandCursor + hoverEnabled: true + + onPressed: event => { + if (disabled) + return; + + rippleAnim.x = event.x; + rippleAnim.y = event.y; + + const dist = (ox, oy) => ox * ox + oy * oy; + rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); + + rippleAnim.restart(); + } + + onClicked: event => !disabled && onClicked(event) + + SequentialAnimation { + id: rippleAnim + + property real x + property real y + property real radius + + PropertyAction { + target: ripple + property: "x" + value: rippleAnim.x + } + PropertyAction { + target: ripple + property: "y" + value: rippleAnim.y + } + PropertyAction { + target: ripple + property: "opacity" + value: 0.08 + } + Anim { + target: ripple + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: ripple + property: "opacity" + to: 0 + } + } + + StyledClippingRect { + id: hoverLayer + + anchors.fill: parent + + color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) + radius: root.radius + + StyledRect { + id: ripple + + radius: Appearance.rounding.full + color: root.color + opacity: 0 + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } +} diff --git a/.github/components/StyledClippingRect.qml b/.github/components/StyledClippingRect.qml new file mode 100644 index 000000000..8f2630c13 --- /dev/null +++ b/.github/components/StyledClippingRect.qml @@ -0,0 +1,12 @@ +import Quickshell.Widgets +import QtQuick + +ClippingRectangle { + id: root + + color: "transparent" + + Behavior on color { + CAnim {} + } +} diff --git a/.github/components/StyledRect.qml b/.github/components/StyledRect.qml new file mode 100644 index 000000000..f5d514395 --- /dev/null +++ b/.github/components/StyledRect.qml @@ -0,0 +1,11 @@ +import QtQuick + +Rectangle { + id: root + + color: "transparent" + + Behavior on color { + CAnim {} + } +} diff --git a/.github/components/StyledText.qml b/.github/components/StyledText.qml new file mode 100644 index 000000000..ed961d26a --- /dev/null +++ b/.github/components/StyledText.qml @@ -0,0 +1,48 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.config +import QtQuick + +Text { + id: root + + property bool animate: false + property string animateProp: "scale" + property real animateFrom: 0 + property real animateTo: 1 + property int animateDuration: Appearance.anim.durations.normal + + renderType: Text.NativeRendering + textFormat: Text.PlainText + color: Colours.palette.m3onSurface + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + + Behavior on color { + CAnim {} + } + + Behavior on text { + enabled: root.animate + + SequentialAnimation { + Anim { + to: root.animateFrom + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + PropertyAction {} + Anim { + to: root.animateTo + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + + component Anim: NumberAnimation { + target: root + property: root.animateProp + duration: root.animateDuration / 2 + easing.type: Easing.BezierSpline + } +} diff --git a/.github/components/containers/StyledFlickable.qml b/.github/components/containers/StyledFlickable.qml new file mode 100644 index 000000000..bc6ae0f62 --- /dev/null +++ b/.github/components/containers/StyledFlickable.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick + +Flickable { + id: root + + maximumFlickVelocity: 3000 + + rebound: Transition { + Anim { + properties: "x,y" + } + } +} diff --git a/.github/components/containers/StyledListView.qml b/.github/components/containers/StyledListView.qml new file mode 100644 index 000000000..626d20635 --- /dev/null +++ b/.github/components/containers/StyledListView.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick + +ListView { + id: root + + maximumFlickVelocity: 3000 + + rebound: Transition { + Anim { + properties: "x,y" + } + } +} diff --git a/.github/components/containers/StyledWindow.qml b/.github/components/containers/StyledWindow.qml new file mode 100644 index 000000000..8c6e39fc8 --- /dev/null +++ b/.github/components/containers/StyledWindow.qml @@ -0,0 +1,9 @@ +import Quickshell +import Quickshell.Wayland + +PanelWindow { + required property string name + + WlrLayershell.namespace: `caelestia-${name}` + color: "transparent" +} diff --git a/.github/components/controls/CircularIndicator.qml b/.github/components/controls/CircularIndicator.qml new file mode 100644 index 000000000..957899e5c --- /dev/null +++ b/.github/components/controls/CircularIndicator.qml @@ -0,0 +1,108 @@ +import ".." +import qs.services +import qs.config +import Caelestia.Internal +import QtQuick +import QtQuick.Templates + +BusyIndicator { + id: root + + enum AnimType { + Advance = 0, + Retreat + } + + enum AnimState { + Stopped, + Running, + Completing + } + + property real implicitSize: Appearance.font.size.normal * 3 + property real strokeWidth: Appearance.padding.small * 0.8 + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + property alias type: manager.indeterminateAnimationType + readonly property alias progress: manager.progress + + property real internalStrokeWidth: strokeWidth + property int animState + + padding: 0 + implicitWidth: implicitSize + implicitHeight: implicitSize + + Component.onCompleted: { + if (running) { + running = false; + running = true; + } + } + + onRunningChanged: { + if (running) { + manager.completeEndProgress = 0; + animState = CircularIndicator.Running; + } else { + if (animState == CircularIndicator.Running) + animState = CircularIndicator.Completing; + } + } + + states: State { + name: "stopped" + when: !root.running + + PropertyChanges { + root.opacity: 0 + root.internalStrokeWidth: root.strokeWidth / 3 + } + } + + transitions: Transition { + Anim { + properties: "opacity,internalStrokeWidth" + duration: manager.completeEndDuration * Appearance.anim.durations.scale + } + } + + contentItem: CircularProgress { + anchors.fill: parent + strokeWidth: root.internalStrokeWidth + fgColour: root.fgColour + bgColour: root.bgColour + padding: root.padding + rotation: manager.rotation + startAngle: manager.startFraction * 360 + value: manager.endFraction - manager.startFraction + } + + CircularIndicatorManager { + id: manager + } + + NumberAnimation { + running: root.animState !== CircularIndicator.Stopped + loops: Animation.Infinite + target: manager + property: "progress" + from: 0 + to: 1 + duration: manager.duration * Appearance.anim.durations.scale + } + + NumberAnimation { + running: root.animState === CircularIndicator.Completing + target: manager + property: "completeEndProgress" + from: 0 + to: 1 + duration: manager.completeEndDuration * Appearance.anim.durations.scale + onFinished: { + if (root.animState === CircularIndicator.Completing) + root.animState = CircularIndicator.Stopped; + } + } +} diff --git a/.github/components/controls/CircularProgress.qml b/.github/components/controls/CircularProgress.qml new file mode 100644 index 000000000..a15cd900b --- /dev/null +++ b/.github/components/controls/CircularProgress.qml @@ -0,0 +1,69 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +Shape { + id: root + + property real value + property int startAngle: -90 + property int strokeWidth: Appearance.padding.smaller + property int padding: 0 + property int spacing: Appearance.spacing.small + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + readonly property real size: Math.min(width, height) + readonly property real arcRadius: (size - padding - strokeWidth) / 2 + readonly property real vValue: value || 1 / 360 + readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: root.strokeWidth + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle + 360 * root.vValue + root.gapAngle + sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + } + + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } + } + + ShapePath { + fillColor: "transparent" + strokeColor: root.fgColour + strokeWidth: root.strokeWidth + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle + sweepAngle: 360 * root.vValue + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + } + + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } + } +} diff --git a/.github/components/controls/CollapsibleSection.qml b/.github/components/controls/CollapsibleSection.qml new file mode 100644 index 000000000..e3d8eefd1 --- /dev/null +++ b/.github/components/controls/CollapsibleSection.qml @@ -0,0 +1,132 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + property bool expanded: false + property bool showBackground: false + property bool nested: false + + signal toggleRequested + + spacing: Appearance.spacing.small + Layout.fillWidth: true + + Item { + id: sectionHeaderItem + Layout.fillWidth: true + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) + + RowLayout { + id: titleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + StyledText { + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: root.expanded ? 180 : 0 + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + Behavior on rotation { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + + StateLayer { + anchors.fill: parent + color: Colours.palette.m3onSurface + radius: Appearance.rounding.normal + showHoverBackground: false + function onClicked(): void { + root.toggleRequested(); + root.expanded = !root.expanded; + } + } + } + + default property alias content: contentColumn.data + + Item { + id: contentWrapper + Layout.fillWidth: true + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + StyledRect { + id: backgroundRect + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + visible: root.showBackground + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + ColumnLayout { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + y: Appearance.spacing.small + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.bottomMargin: Appearance.spacing.small + spacing: Appearance.spacing.small + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + StyledText { + id: descriptionText + Layout.fillWidth: true + Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 + Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + visible: root.description !== "" + text: root.description + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + } + } + } +} diff --git a/.github/components/controls/CustomMouseArea.qml b/.github/components/controls/CustomMouseArea.qml new file mode 100644 index 000000000..7c973c244 --- /dev/null +++ b/.github/components/controls/CustomMouseArea.qml @@ -0,0 +1,21 @@ +import QtQuick + +MouseArea { + property int scrollAccumulatedY: 0 + + function onWheel(event: WheelEvent): void { + } + + onWheel: event => { + // Update accumulated scroll + if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) + scrollAccumulatedY = 0; + scrollAccumulatedY += event.angleDelta.y; + + // Trigger handler and reset if above threshold + if (Math.abs(scrollAccumulatedY) >= 120) { + onWheel(event); + scrollAccumulatedY = 0; + } + } +} diff --git a/.github/components/controls/CustomSpinBox.qml b/.github/components/controls/CustomSpinBox.qml new file mode 100644 index 000000000..438dc0806 --- /dev/null +++ b/.github/components/controls/CustomSpinBox.qml @@ -0,0 +1,170 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + property real value + property real max: Infinity + property real min: -Infinity + property real step: 1 + property alias repeatRate: timer.interval + + signal valueModified(value: real) + + spacing: Appearance.spacing.small + + property bool isEditing: false + property string displayText: root.value.toString() + + onValueChanged: { + if (!root.isEditing) { + root.displayText = root.value.toString(); + } + } + + StyledTextField { + id: textField + + inputMethodHints: Qt.ImhFormattedNumbersOnly + text: root.isEditing ? text : root.displayText + validator: DoubleValidator { + bottom: root.min + top: root.max + decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0 + } + onActiveFocusChanged: { + if (activeFocus) { + root.isEditing = true; + } else { + root.isEditing = false; + root.displayText = root.value.toString(); + } + } + onAccepted: { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + root.isEditing = false; + } + onEditingFinished: { + if (text !== root.displayText) { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + } + root.isEditing = false; + } + + padding: Appearance.padding.small + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + implicitWidth: 100 + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: upState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + let newValue = Math.min(root.max, root.value + root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "keyboard_arrow_up" + color: Colours.palette.m3onPrimary + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: downState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + let newValue = Math.max(root.min, root.value - root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + } + + MaterialIcon { + id: downIcon + + anchors.centerIn: parent + text: "keyboard_arrow_down" + color: Colours.palette.m3onPrimary + } + } + + Timer { + id: timer + + interval: 100 + repeat: true + triggeredOnStart: true + onTriggered: { + if (upState.pressed) + upState.onClicked(); + else if (downState.pressed) + downState.onClicked(); + } + } +} diff --git a/.github/components/controls/FilledSlider.qml b/.github/components/controls/FilledSlider.qml new file mode 100644 index 000000000..80dd44c5f --- /dev/null +++ b/.github/components/controls/FilledSlider.qml @@ -0,0 +1,146 @@ +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +Slider { + id: root + + required property string icon + property real oldValue + property bool initialized + + orientation: Qt.Vertical + + background: StyledRect { + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.full + + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + + y: root.handle.y + implicitHeight: parent.height - y + + color: Colours.palette.m3secondary + radius: parent.radius + } + } + + handle: Item { + id: handle + + property alias moving: icon.moving + + y: root.visualPosition * (root.availableHeight - height) + implicitWidth: root.width + implicitHeight: root.width + + Elevation { + anchors.fill: parent + radius: rect.radius + level: handleInteraction.containsMouse ? 2 : 1 + } + + StyledRect { + id: rect + + anchors.fill: parent + + color: Colours.palette.m3inverseSurface + radius: Appearance.rounding.full + + MouseArea { + id: handleInteraction + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.NoButton + } + + MaterialIcon { + id: icon + + property bool moving + + function update(): void { + animate = !moving; + binding.when = moving; + font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; + font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + } + + text: root.icon + color: Colours.palette.m3inverseOnSurface + anchors.centerIn: parent + + onMovingChanged: anim.restart() + + Binding { + id: binding + + target: icon + property: "text" + value: Math.round(root.value * 100) + when: false + } + + SequentialAnimation { + id: anim + + Anim { + target: icon + property: "scale" + to: 0 + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + ScriptAction { + script: icon.update() + } + Anim { + target: icon + property: "scale" + to: 1 + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + } + + onPressedChanged: handle.moving = pressed + + onValueChanged: { + if (!initialized) { + initialized = true; + return; + } + if (Math.abs(value - oldValue) < 0.01) + return; + oldValue = value; + handle.moving = true; + stateChangeDelay.restart(); + } + + Timer { + id: stateChangeDelay + + interval: 500 + onTriggered: { + if (!root.pressed) + handle.moving = false; + } + } + + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } +} diff --git a/.github/components/controls/IconButton.qml b/.github/components/controls/IconButton.qml new file mode 100644 index 000000000..ffb1d0663 --- /dev/null +++ b/.github/components/controls/IconButton.qml @@ -0,0 +1,83 @@ +import ".." +import qs.services +import qs.config +import QtQuick + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias icon: label.text + property bool checked + property bool toggle + property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller + property alias font: label.font + property int type: IconButton.Filled + property bool disabled + + property alias stateLayer: stateLayer + property alias label: label + property alias radiusAnim: radiusAnim + + property bool internalChecked + property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: { + if (!toggle && type === IconButton.Filled) + return Colours.palette.m3primary; + return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; + } + property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary + property color inactiveOnColour: { + if (!toggle && type === IconButton.Filled) + return Colours.palette.m3onPrimary; + return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant; + } + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + + implicitWidth: implicitHeight + implicitHeight: label.implicitHeight + padding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + MaterialIcon { + id: label + + anchors.centerIn: parent + color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour + fill: !root.toggle || root.internalChecked ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + Behavior on radius { + Anim { + id: radiusAnim + } + } +} diff --git a/.github/components/controls/IconTextButton.qml b/.github/components/controls/IconTextButton.qml new file mode 100644 index 000000000..b2bb96cc0 --- /dev/null +++ b/.github/components/controls/IconTextButton.qml @@ -0,0 +1,88 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias icon: iconLabel.text + property alias text: label.text + property bool checked + property bool toggle + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property alias font: label.font + property int type: IconTextButton.Filled + + property alias stateLayer: stateLayer + property alias iconLabel: iconLabel + property alias label: label + + property bool internalChecked + property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer + property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary + property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: row.implicitWidth + horizontalPadding * 2 + implicitHeight: row.implicitHeight + verticalPadding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + RowLayout { + id: row + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconLabel + + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + fill: root.internalChecked ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + id: label + + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + } + } + + Behavior on radius { + Anim {} + } +} diff --git a/.github/components/controls/Menu.qml b/.github/components/controls/Menu.qml new file mode 100644 index 000000000..c763b54a8 --- /dev/null +++ b/.github/components/controls/Menu.qml @@ -0,0 +1,113 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Elevation { + id: root + + property list items + property MenuItem active: items[0] ?? null + property bool expanded + + signal itemSelected(item: MenuItem) + + radius: Appearance.rounding.small / 2 + level: 2 + + implicitWidth: Math.max(200, column.implicitWidth) + implicitHeight: root.expanded ? column.implicitHeight : 0 + opacity: root.expanded ? 1 : 0 + + StyledClippingRect { + anchors.fill: parent + radius: parent.radius + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: column + + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + Repeater { + model: root.items + + StyledRect { + id: item + + required property int index + required property MenuItem modelData + readonly property bool active: modelData === root.active + + Layout.fillWidth: true + implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 + + color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) + + StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded + + function onClicked(): void { + root.itemSelected(item.modelData); + root.active = item.modelData; + root.expanded = false; + } + } + + RowLayout { + id: menuOptionRow + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: item.modelData.icon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: item.modelData.text + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + Loader { + Layout.alignment: Qt.AlignVCenter + active: item.modelData.trailingIcon.length > 0 + visible: active + + sourceComponent: MaterialIcon { + text: item.modelData.trailingIcon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + } + } + } + } + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } +} diff --git a/.github/components/controls/MenuItem.qml b/.github/components/controls/MenuItem.qml new file mode 100644 index 000000000..5348bbe82 --- /dev/null +++ b/.github/components/controls/MenuItem.qml @@ -0,0 +1,11 @@ +import QtQuick + +QtObject { + required property string text + property string icon + property string trailingIcon + property string activeIcon: icon + property string activeText: text + + signal clicked +} diff --git a/.github/components/controls/SpinBoxRow.qml b/.github/components/controls/SpinBoxRow.qml new file mode 100644 index 000000000..fe6a19822 --- /dev/null +++ b/.github/components/controls/SpinBoxRow.qml @@ -0,0 +1,52 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property real value + required property real min + required property real max + property real step: 1 + property var onValueModified: function (value) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + CustomSpinBox { + min: root.min + max: root.max + step: root.step + value: root.value + onValueModified: value => { + root.onValueModified(value); + } + } + } +} diff --git a/.github/components/controls/SplitButton.qml b/.github/components/controls/SplitButton.qml new file mode 100644 index 000000000..c91474eae --- /dev/null +++ b/.github/components/controls/SplitButton.qml @@ -0,0 +1,164 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Row { + id: root + + enum Type { + Filled, + Tonal + } + + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property int type: SplitButton.Filled + property bool disabled + property bool menuOnTop + property string fallbackIcon + property string fallbackText + + property alias menuItems: menu.items + property alias active: menu.active + property alias expanded: menu.expanded + property alias menu: menu + property alias iconLabel: iconLabel + property alias label: label + property alias stateLayer: stateLayer + + property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer + property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + spacing: Math.floor(Appearance.spacing.small / 2) + + StyledRect { + radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + topRightRadius: Appearance.rounding.small / 2 + bottomRightRadius: Appearance.rounding.small / 2 + color: root.disabled ? root.disabledColour : root.colour + + implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 + implicitHeight: expandBtn.implicitHeight + + StateLayer { + id: stateLayer + + rect.topRightRadius: parent.topRightRadius + rect.bottomRightRadius: parent.bottomRightRadius + color: root.textColour + disabled: root.disabled + + function onClicked(): void { + root.active?.clicked(); + } + } + + RowLayout { + id: textRow + + anchors.centerIn: parent + anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconLabel + + Layout.alignment: Qt.AlignVCenter + animate: true + text: root.active?.activeIcon ?? root.fallbackIcon + color: root.disabled ? root.disabledTextColour : root.textColour + fill: 1 + } + + StyledText { + id: label + + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: implicitWidth + animate: true + text: root.active?.activeText ?? root.fallbackText + color: root.disabled ? root.disabledTextColour : root.textColour + clip: true + + Behavior on Layout.preferredWidth { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + } + } + + StyledRect { + id: expandBtn + + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 + + radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + topLeftRadius: rad + bottomLeftRadius: rad + color: root.disabled ? root.disabledColour : root.colour + + implicitWidth: implicitHeight + implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2 + + StateLayer { + id: expandStateLayer + + rect.topLeftRadius: parent.topLeftRadius + rect.bottomLeftRadius: parent.bottomLeftRadius + color: root.textColour + disabled: root.disabled + + function onClicked(): void { + root.expanded = !root.expanded; + } + } + + MaterialIcon { + id: expandIcon + + anchors.centerIn: parent + anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4) + + text: "expand_more" + color: root.disabled ? root.disabledTextColour : root.textColour + rotation: root.expanded ? 180 : 0 + + Behavior on anchors.horizontalCenterOffset { + Anim {} + } + + Behavior on rotation { + Anim {} + } + } + + Behavior on rad { + Anim {} + } + + Menu { + id: menu + + states: State { + when: root.menuOnTop + + AnchorChanges { + target: menu + anchors.top: undefined + anchors.bottom: expandBtn.top + } + } + + anchors.top: parent.bottom + anchors.right: parent.right + anchors.topMargin: Appearance.spacing.small + anchors.bottomMargin: Appearance.spacing.small + } + } +} diff --git a/.github/components/controls/SplitButtonRow.qml b/.github/components/controls/SplitButtonRow.qml new file mode 100644 index 000000000..db9925ff6 --- /dev/null +++ b/.github/components/controls/SplitButtonRow.qml @@ -0,0 +1,62 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + property int expandedZ: 100 + property bool enabled: true + + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active + property alias expanded: splitButton.expanded + property alias type: splitButton.type + + signal selected(item: MenuItem) + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + clip: false + z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 + opacity: enabled ? 1.0 : 0.5 + + RowLayout { + id: row + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + color: root.enabled ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + } + + SplitButton { + id: splitButton + enabled: root.enabled + type: SplitButton.Filled + + menu.z: 1 + + stateLayer.onClicked: { + splitButton.expanded = !splitButton.expanded; + } + + menu.onItemSelected: item => { + root.selected(item); + } + } + } +} diff --git a/.github/components/controls/StyledInputField.qml b/.github/components/controls/StyledInputField.qml new file mode 100644 index 000000000..0d199c738 --- /dev/null +++ b/.github/components/controls/StyledInputField.qml @@ -0,0 +1,79 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.services +import qs.config +import QtQuick + +Item { + id: root + + property string text: "" + property var validator: null + property bool readOnly: false + property int horizontalAlignment: TextInput.AlignHCenter + property int implicitWidth: 70 + property bool enabled: true + + // Expose activeFocus through alias to avoid FINAL property override + readonly property alias hasFocus: inputField.activeFocus + + signal textEdited(string text) + signal editingFinished + + implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + id: container + + anchors.fill: parent + color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + opacity: root.enabled ? 1 : 0.5 + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + MouseArea { + id: inputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + enabled: root.enabled + } + + StyledTextField { + id: inputField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: root.horizontalAlignment + validator: root.validator + readOnly: root.readOnly + enabled: root.enabled + + Binding { + target: inputField + property: "text" + value: root.text + when: !inputField.activeFocus + } + + onTextChanged: { + root.text = text; + root.textEdited(text); + } + + onEditingFinished: { + root.editingFinished(); + } + } + } +} diff --git a/.github/components/controls/StyledRadioButton.qml b/.github/components/controls/StyledRadioButton.qml new file mode 100644 index 000000000..b72fc77f3 --- /dev/null +++ b/.github/components/controls/StyledRadioButton.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +RadioButton { + id: root + + font.pointSize: Appearance.font.size.smaller + + implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin + implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) + + indicator: Rectangle { + id: outerCircle + + implicitWidth: 20 + implicitHeight: 20 + radius: Appearance.rounding.full + color: "transparent" + border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + border.width: 2 + anchors.verticalCenter: parent.verticalCenter + + StateLayer { + anchors.margins: -Appearance.padding.smaller + color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary + z: -1 + + function onClicked(): void { + root.click(); + } + } + + StyledRect { + anchors.centerIn: parent + implicitWidth: 8 + implicitHeight: 8 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) + } + + Behavior on border.color { + CAnim {} + } + } + + contentItem: StyledText { + text: root.text + font.pointSize: root.font.pointSize + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right + anchors.leftMargin: Appearance.spacing.smaller + } +} diff --git a/.github/components/controls/StyledScrollBar.qml b/.github/components/controls/StyledScrollBar.qml new file mode 100644 index 000000000..de8b679cd --- /dev/null +++ b/.github/components/controls/StyledScrollBar.qml @@ -0,0 +1,190 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +ScrollBar { + id: root + + required property Flickable flickable + property bool shouldBeActive + property real nonAnimPosition + property bool animating + + onHoveredChanged: { + if (hovered) + shouldBeActive = true; + else + shouldBeActive = flickable.moving; + } + + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false + + // Sync nonAnimPosition with Qt's automatic position binding + onPositionChanged: { + if (_updatingFromUser) { + _updatingFromUser = false; + return; + } + if (position === nonAnimPosition) { + animating = false; + return; + } + if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { + nonAnimPosition = position; + } + } + + // Sync nonAnimPosition with flickable when not animating + Connections { + target: flickable + function onContentYChanged() { + if (!animating && !fullMouse.pressed) { + _updatingFromFlickable = true; + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } else { + nonAnimPosition = 0; + } + _updatingFromFlickable = false; + } + } + } + + Component.onCompleted: { + if (flickable) { + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } + } + } + implicitWidth: Appearance.padding.small + + contentItem: StyledRect { + anchors.left: parent.left + anchors.right: parent.right + opacity: { + if (root.size === 1) + return 0; + if (fullMouse.pressed) + return 1; + if (mouse.containsMouse) + return 0.8; + if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive) + return 0.6; + return 0; + } + radius: Appearance.rounding.full + color: Colours.palette.m3secondary + + MouseArea { + id: mouse + + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + + Behavior on opacity { + Anim {} + } + } + + Connections { + target: root.flickable + + function onMovingChanged(): void { + if (root.flickable.moving) + root.shouldBeActive = true; + else + hideDelay.restart(); + } + } + + Timer { + id: hideDelay + + interval: 600 + onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered + } + + CustomMouseArea { + id: fullMouse + + anchors.fill: parent + preventStealing: true + + onPressed: event => { + root.animating = true; + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + + onPositionChanged: event => { + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + + function onWheel(event: WheelEvent): void { + root.animating = true; + root._updatingFromUser = true; + let newPos = root.nonAnimPosition; + if (event.angleDelta.y > 0) + newPos = Math.max(0, root.nonAnimPosition - 0.1); + else if (event.angleDelta.y < 0) + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + } + + Behavior on position { + enabled: !fullMouse.pressed + + Anim {} + } +} diff --git a/.github/components/controls/StyledSlider.qml b/.github/components/controls/StyledSlider.qml new file mode 100644 index 000000000..0ef229df2 --- /dev/null +++ b/.github/components/controls/StyledSlider.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +Slider { + id: root + + background: Item { + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.topMargin: root.implicitHeight / 3 + anchors.bottomMargin: root.implicitHeight / 3 + + implicitWidth: root.handle.x - root.implicitHeight / 6 + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + topRightRadius: root.implicitHeight / 15 + bottomRightRadius: root.implicitHeight / 15 + } + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.topMargin: root.implicitHeight / 3 + anchors.bottomMargin: root.implicitHeight / 3 + + implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 + + color: Colours.palette.m3surfaceContainerHighest + radius: Appearance.rounding.full + topLeftRadius: root.implicitHeight / 15 + bottomLeftRadius: root.implicitHeight / 15 + } + } + + handle: StyledRect { + x: root.visualPosition * root.availableWidth - implicitWidth / 2 + + implicitWidth: root.implicitHeight / 4.5 + implicitHeight: root.implicitHeight + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + } + } +} diff --git a/.github/components/controls/StyledSwitch.qml b/.github/components/controls/StyledSwitch.qml new file mode 100644 index 000000000..ce93cd505 --- /dev/null +++ b/.github/components/controls/StyledSwitch.qml @@ -0,0 +1,152 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates +import QtQuick.Shapes + +Switch { + id: root + + property int cLayer: 1 + + implicitWidth: implicitIndicatorWidth + implicitHeight: implicitIndicatorHeight + + indicator: StyledRect { + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) + + implicitWidth: implicitHeight * 1.7 + implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + + StyledRect { + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) + + x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 + implicitWidth: nonAnimWidth + implicitHeight: parent.implicitHeight - Appearance.padding.small + anchors.verticalCenter: parent.verticalCenter + + StyledRect { + anchors.fill: parent + radius: parent.radius + + color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurface + opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 + + Behavior on opacity { + Anim {} + } + } + + Shape { + id: icon + + property point start1: { + if (root.pressed) + return Qt.point(width * 0.2, height / 2); + if (root.checked) + return Qt.point(width * 0.15, height / 2); + return Qt.point(width * 0.15, height * 0.15); + } + property point end1: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.8, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.85, height * 0.85); + } + property point start2: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.2, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.15, height * 0.85); + } + property point end2: { + if (root.pressed) + return Qt.point(width * 0.8, height / 2); + if (root.checked) + return Qt.point(width * 0.85, height * 0.2); + return Qt.point(width * 0.85, height * 0.15); + } + + anchors.centerIn: parent + width: height + height: parent.implicitHeight - Appearance.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: Appearance.font.size.larger * 0.15 + strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest + fillColor: "transparent" + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + startX: icon.start1.x + startY: icon.start1.y + + PathLine { + x: icon.end1.x + y: icon.end1.y + } + PathMove { + x: icon.start2.x + y: icon.start2.y + } + PathLine { + x: icon.end2.x + y: icon.end2.y + } + + Behavior on strokeColor { + CAnim {} + } + } + + Behavior on start1 { + PropAnim {} + } + Behavior on end1 { + PropAnim {} + } + Behavior on start2 { + PropAnim {} + } + Behavior on end2 { + PropAnim {} + } + } + + Behavior on x { + Anim {} + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + + component PropAnim: PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/.github/components/controls/StyledTextField.qml b/.github/components/controls/StyledTextField.qml new file mode 100644 index 000000000..60bcff259 --- /dev/null +++ b/.github/components/controls/StyledTextField.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls + +TextField { + id: root + + color: Colours.palette.m3onSurface + placeholderTextColor: Colours.palette.m3outline + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering + cursorVisible: !readOnly + + background: null + + cursorDelegate: StyledRect { + id: cursor + + property bool disableBlink + + implicitWidth: 2 + color: Colours.palette.m3primary + radius: Appearance.rounding.normal + + Connections { + target: root + + function onCursorPositionChanged(): void { + if (root.activeFocus && root.cursorVisible) { + cursor.opacity = 1; + cursor.disableBlink = true; + enableBlink.restart(); + } + } + } + + Timer { + id: enableBlink + + interval: 100 + onTriggered: cursor.disableBlink = false + } + + Timer { + running: root.activeFocus && root.cursorVisible && !cursor.disableBlink + repeat: true + triggeredOnStart: true + interval: 500 + onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 + } + + Binding { + when: !root.activeFocus || !root.cursorVisible + cursor.opacity: 0 + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + } + + Behavior on color { + CAnim {} + } + + Behavior on placeholderTextColor { + CAnim {} + } +} diff --git a/.github/components/controls/SwitchRow.qml b/.github/components/controls/SwitchRow.qml new file mode 100644 index 000000000..6dda3f0cc --- /dev/null +++ b/.github/components/controls/SwitchRow.qml @@ -0,0 +1,48 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property bool checked + property bool enabled: true + property var onToggled: function (checked) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + checked: root.checked + enabled: root.enabled + onToggled: { + root.onToggled(checked); + } + } + } +} diff --git a/.github/components/controls/TextButton.qml b/.github/components/controls/TextButton.qml new file mode 100644 index 000000000..ecf7eb133 --- /dev/null +++ b/.github/components/controls/TextButton.qml @@ -0,0 +1,78 @@ +import ".." +import qs.services +import qs.config +import QtQuick + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias text: label.text + property bool checked + property bool toggle + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property alias font: label.font + property int type: TextButton.Filled + + property alias stateLayer: stateLayer + property alias label: label + + property bool internalChecked + property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: { + if (!toggle && type === TextButton.Filled) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; + } + property color activeOnColour: { + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; + } + property color inactiveOnColour: { + if (!toggle && type === TextButton.Filled) + return Colours.palette.m3onPrimary; + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; + } + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: label.implicitWidth + horizontalPadding * 2 + implicitHeight: label.implicitHeight + verticalPadding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + StyledText { + id: label + + anchors.centerIn: parent + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + } + + Behavior on radius { + Anim {} + } +} diff --git a/.github/components/controls/ToggleButton.qml b/.github/components/controls/ToggleButton.qml new file mode 100644 index 000000000..98c7564f2 --- /dev/null +++ b/.github/components/controls/ToggleButton.qml @@ -0,0 +1,124 @@ +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + property real iconSize: Appearance.font.size.large + property real horizontalPadding: Appearance.padding.large + property real verticalPadding: Appearance.padding.normal + property string tooltip: "" + + property bool hovered: false + signal clicked + + Component.onCompleted: { + hovered = toggleStateLayer.containsMouse; + } + + Connections { + target: toggleStateLayer + function onContainsMouseChanged() { + const newHovered = toggleStateLayer.containsMouse; + if (hovered !== newHovered) { + hovered = newHovered; + } + } + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 + implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + + function onClicked(): void { + root.clicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: root.toggled ? 1 : 0 + text: root.icon + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + font.pointSize: root.iconSize + + Behavior on fill { + Anim {} + } + } + + Loader { + active: !!root.label + visible: active + + sourceComponent: StyledText { + text: root.label + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + // Tooltip - positioned absolutely, doesn't affect layout + Loader { + id: tooltipLoader + active: root.tooltip !== "" + z: 10000 + width: 0 + height: 0 + sourceComponent: Component { + Tooltip { + target: root + text: root.tooltip + } + } + // Completely remove from layout + Layout.fillWidth: false + Layout.fillHeight: false + Layout.preferredWidth: 0 + Layout.preferredHeight: 0 + Layout.maximumWidth: 0 + Layout.maximumHeight: 0 + Layout.minimumWidth: 0 + Layout.minimumHeight: 0 + } +} diff --git a/.github/components/controls/ToggleRow.qml b/.github/components/controls/ToggleRow.qml new file mode 100644 index 000000000..269d3d6a5 --- /dev/null +++ b/.github/components/controls/ToggleRow.qml @@ -0,0 +1,28 @@ +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } +} diff --git a/.github/components/controls/Tooltip.qml b/.github/components/controls/Tooltip.qml new file mode 100644 index 000000000..b129a37b9 --- /dev/null +++ b/.github/components/controls/Tooltip.qml @@ -0,0 +1,185 @@ +import ".." +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Popup { + id: root + + required property Item target + required property string text + property int delay: 500 + property int timeout: 0 + + property bool tooltipVisible: false + property Timer showTimer: Timer { + interval: root.delay + onTriggered: root.tooltipVisible = true + } + property Timer hideTimer: Timer { + interval: root.timeout + onTriggered: root.tooltipVisible = false + } + + // Popup properties - doesn't affect layout + parent: { + let p = target; + // Walk up to find the root Item (usually has anchors.fill: parent) + while (p && p.parent) { + const parentItem = p.parent; + // Check if this looks like a root pane Item + if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { + return parentItem; + } + p = parentItem; + } + // Fallback + return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; + } + + visible: tooltipVisible + modal: false + closePolicy: Popup.NoAutoClose + padding: 0 + margins: 0 + background: Item {} + + // Update position when target moves or tooltip becomes visible + onTooltipVisibleChanged: { + if (tooltipVisible) { + Qt.callLater(updatePosition); + } + } + Connections { + target: root.target + function onXChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onYChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onWidthChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onHeightChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + } + + function updatePosition() { + if (!target || !parent) + return; + + // Wait for tooltipRect to have its size calculated + Qt.callLater(() => { + if (!target || !parent || !tooltipRect) + return; + + // Get target position in parent's coordinate system + const targetPos = target.mapToItem(parent, 0, 0); + const targetCenterX = targetPos.x + target.width / 2; + + // Get tooltip size (use width/height if available, otherwise implicit) + const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; + const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; + + // Center tooltip horizontally on target + let newX = targetCenterX - tooltipWidth / 2; + + // Position tooltip above target + let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + + // Keep within bounds + const padding = Appearance.padding.normal; + if (newX < padding) { + newX = padding; + } else if (newX + tooltipWidth > (parent.width - padding)) { + newX = parent.width - tooltipWidth - padding; + } + + // Update popup position + x = newX; + y = newY; + }); + } + + enter: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + exit: Transition { + Anim { + property: "opacity" + from: 1 + to: 0 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + // Monitor hover state + Connections { + target: root.target + function onHoveredChanged() { + if (target.hovered) { + showTimer.start(); + if (timeout > 0) { + hideTimer.stop(); + hideTimer.start(); + } + } else { + showTimer.stop(); + hideTimer.stop(); + tooltipVisible = false; + } + } + } + + contentItem: StyledRect { + id: tooltipRect + + implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 + + color: Colours.palette.m3surfaceContainerHighest + radius: Appearance.rounding.small + antialiasing: true + + // Add elevation for depth + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: 3 + } + + StyledText { + id: tooltipText + + anchors.centerIn: parent + + text: root.text + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.small + } + } + + Component.onCompleted: { + if (tooltipVisible) { + updatePosition(); + } + } +} diff --git a/.github/components/effects/ColouredIcon.qml b/.github/components/effects/ColouredIcon.qml new file mode 100644 index 000000000..5ef4d4cc0 --- /dev/null +++ b/.github/components/effects/ColouredIcon.qml @@ -0,0 +1,35 @@ +pragma ComponentBehavior: Bound + +import Caelestia +import Quickshell.Widgets +import QtQuick + +IconImage { + id: root + + required property color colour + + asynchronous: true + + layer.enabled: true + layer.effect: Colouriser { + sourceColor: analyser.dominantColour + colorizationColor: root.colour + } + + layer.onEnabledChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } + + onStatusChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } + + ImageAnalyser { + id: analyser + + sourceItem: root + } +} diff --git a/.github/components/effects/Colouriser.qml b/.github/components/effects/Colouriser.qml new file mode 100644 index 000000000..2948155d6 --- /dev/null +++ b/.github/components/effects/Colouriser.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick +import QtQuick.Effects + +MultiEffect { + property color sourceColor: "black" + + colorization: 1 + brightness: 1 - sourceColor.hslLightness + + Behavior on colorizationColor { + CAnim {} + } +} diff --git a/.github/components/effects/Elevation.qml b/.github/components/effects/Elevation.qml new file mode 100644 index 000000000..fb29f16e8 --- /dev/null +++ b/.github/components/effects/Elevation.qml @@ -0,0 +1,18 @@ +import ".." +import qs.services +import QtQuick +import QtQuick.Effects + +RectangularShadow { + property int level + property real dp: [0, 1, 3, 6, 8, 12][level] + + color: Qt.alpha(Colours.palette.m3shadow, 0.7) + blur: (dp * 5) ** 0.7 + spread: -dp * 0.3 + (dp * 0.1) ** 2 + offset.y: dp / 2 + + Behavior on dp { + Anim {} + } +} diff --git a/.github/components/effects/InnerBorder.qml b/.github/components/effects/InnerBorder.qml new file mode 100644 index 000000000..d4a751f84 --- /dev/null +++ b/.github/components/effects/InnerBorder.qml @@ -0,0 +1,44 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Effects + +StyledRect { + property alias innerRadius: maskInner.radius + property alias thickness: maskInner.anchors.margins + property alias leftThickness: maskInner.anchors.leftMargin + property alias topThickness: maskInner.anchors.topMargin + property alias rightThickness: maskInner.anchors.rightMargin + property alias bottomThickness: maskInner.anchors.bottomMargin + + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + id: maskInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + radius: Appearance.rounding.small + } + } +} diff --git a/.github/components/effects/OpacityMask.qml b/.github/components/effects/OpacityMask.qml new file mode 100644 index 000000000..22e424960 --- /dev/null +++ b/.github/components/effects/OpacityMask.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick + +ShaderEffect { + required property Item source + required property Item maskSource + + fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) +} diff --git a/.github/components/filedialog/CurrentItem.qml b/.github/components/filedialog/CurrentItem.qml new file mode 100644 index 000000000..bb87133c7 --- /dev/null +++ b/.github/components/filedialog/CurrentItem.qml @@ -0,0 +1,102 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +Item { + id: root + + required property var currentItem + + implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 + + Shape { + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: path + + readonly property real rounding: Appearance.rounding.small + readonly property bool flatten: root.implicitHeight < rounding * 2 + readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding + + strokeWidth: -1 + fillColor: Colours.tPalette.m3surfaceContainer + + startX: root.implicitWidth + startY: root.implicitHeight + + PathLine { + relativeX: -(root.implicitWidth + path.rounding) + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: 0 + relativeY: -(root.implicitHeight - path.roundingY * 2) + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + } + PathLine { + relativeX: root.implicitHeight > 0 ? root.implicitWidth - path.rounding * 2 : root.implicitWidth + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.rounding + radiusX: path.rounding + radiusY: path.rounding + direction: PathArc.Counterclockwise + } + + Behavior on fillColor { + CAnim {} + } + } + } + + Item { + anchors.fill: parent + clip: true + + StyledText { + id: content + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small + anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small + + Connections { + target: root + + function onCurrentItemChanged(): void { + if (root.currentItem) + content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); + } + } + } + } + + Behavior on implicitWidth { + enabled: !!root.currentItem + + Anim {} + } + + Behavior on implicitHeight { + Anim {} + } +} diff --git a/.github/components/filedialog/DialogButtons.qml b/.github/components/filedialog/DialogButtons.qml new file mode 100644 index 000000000..bde9ac277 --- /dev/null +++ b/.github/components/filedialog/DialogButtons.qml @@ -0,0 +1,93 @@ +import ".." +import qs.services +import qs.config +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + required property FolderContents folder + + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Filter:") + } + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.rightMargin: Appearance.spacing.normal + + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + StyledText { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` + } + } + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + disabled: !root.dialog.selectionValid + + function onClicked(): void { + root.dialog.accepted(root.folder.currentItem.modelData.path); + } + } + + StyledText { + id: selectText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Select") + color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline + } + } + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + function onClicked(): void { + root.dialog.rejected(); + } + } + + StyledText { + id: cancelText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Cancel") + } + } + } +} diff --git a/.github/components/filedialog/FileDialog.qml b/.github/components/filedialog/FileDialog.qml new file mode 100644 index 000000000..f3187a55b --- /dev/null +++ b/.github/components/filedialog/FileDialog.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.services +import Quickshell +import QtQuick +import QtQuick.Layouts + +LazyLoader { + id: loader + + property list cwd: ["Home"] + property string filterLabel: "All files" + property list filters: ["*"] + property string title: qsTr("Select a file") + + signal accepted(path: string) + signal rejected + + function open(): void { + activeAsync = true; + } + + function close(): void { + rejected(); + } + + onAccepted: activeAsync = false + onRejected: activeAsync = false + + FloatingWindow { + id: root + + property list cwd: loader.cwd + property string filterLabel: loader.filterLabel + property list filters: loader.filters + + readonly property bool selectionValid: { + const file = folderContents.currentItem?.modelData; + return (file && !file.isDir && (filters.includes("*") || filters.includes(file.suffix))) ?? false; + } + + function accepted(path: string): void { + loader.accepted(path); + } + + function rejected(): void { + loader.rejected(); + } + + implicitWidth: 1000 + implicitHeight: 600 + color: Colours.tPalette.m3surface + title: loader.title + + onVisibleChanged: { + if (!visible) + rejected(); + } + + RowLayout { + anchors.fill: parent + + spacing: 0 + + Sidebar { + Layout.fillHeight: true + dialog: root + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: 0 + + HeaderBar { + Layout.fillWidth: true + dialog: root + } + + FolderContents { + id: folderContents + + Layout.fillWidth: true + Layout.fillHeight: true + dialog: root + } + + DialogButtons { + Layout.fillWidth: true + dialog: root + folder: folderContents + } + } + } + + Behavior on color { + CAnim {} + } + } +} diff --git a/.github/components/filedialog/FolderContents.qml b/.github/components/filedialog/FolderContents.qml new file mode 100644 index 000000000..e16c7a15c --- /dev/null +++ b/.github/components/filedialog/FolderContents.qml @@ -0,0 +1,228 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../controls" +import "../images" +import qs.services +import qs.config +import qs.utils +import Caelestia.Models +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +Item { + id: root + + required property var dialog + property alias currentItem: view.currentItem + + StyledRect { + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.small + radius: Appearance.rounding.small + } + } + + Loader { + anchors.centerIn: parent + + opacity: view.count === 0 ? 1 : 0 + active: opacity > 0 + + sourceComponent: ColumnLayout { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + } + + StyledText { + text: qsTr("This folder is empty") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + Behavior on opacity { + Anim {} + } + } + + GridView { + id: view + + anchors.fill: parent + anchors.margins: Appearance.padding.small + Appearance.padding.normal + + cellWidth: Sizes.itemWidth + Appearance.spacing.small + cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + + clip: true + focus: true + currentIndex: -1 + Keys.onEscapePressed: currentIndex = -1 + + Keys.onReturnPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + Keys.onEnterPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + model: FileSystemModel { + path: { + if (root.dialog.cwd[0] === "Home") + return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; + else + return root.dialog.cwd.join("/"); + } + onPathChanged: view.currentIndex = -1 + } + + delegate: StyledRect { + id: item + + required property int index + required property FileSystemEntry modelData + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + onDoubleClicked: { + if (item.modelData.isDir) + root.dialog.cwd.push(item.modelData.name); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.modelData.path); + } + + function onClicked(): void { + view.currentIndex = item.index; + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Appearance.padding.normal + + implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + + Component.onCompleted: { + const file = item.modelData; + if (file.isImage) + source = Qt.resolvedUrl(file.path); + else if (!file.isDir) + source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); + else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) + source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); + else + source = Quickshell.iconPath("inode-directory"); + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small + anchors.margins: Appearance.padding.normal + + horizontalAlignment: Text.AlignHCenter + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + + Component.onCompleted: text = item.modelData.name + } + + Behavior on implicitHeight { + Anim {} + } + } + + add: Transition { + Anim { + properties: "opacity,scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + remove: Transition { + Anim { + property: "opacity" + to: 0 + } + Anim { + property: "scale" + to: 0.5 + } + } + + displaced: Transition { + Anim { + properties: "opacity,scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + + CurrentItem { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.small + + currentItem: view.currentItem + } +} diff --git a/.github/components/filedialog/HeaderBar.qml b/.github/components/filedialog/HeaderBar.qml new file mode 100644 index 000000000..c9a3feb53 --- /dev/null +++ b/.github/components/filedialog/HeaderBar.qml @@ -0,0 +1,139 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + Item { + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + radius: Appearance.rounding.small + disabled: root.dialog.cwd.length === 1 + + function onClicked(): void { + root.dialog.cwd.pop(); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "drive_folder_upload" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface + grade: 200 + } + } + + StyledRect { + Layout.fillWidth: true + + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + + implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 + + RowLayout { + id: pathComponents + + anchors.fill: parent + anchors.margins: Appearance.padding.small / 2 + anchors.leftMargin: 0 + + spacing: Appearance.spacing.small + + Repeater { + model: root.dialog.cwd + + RowLayout { + id: folder + + required property string modelData + required property int index + + spacing: 0 + + Loader { + Layout.rightMargin: Appearance.spacing.small + active: folder.index > 0 + sourceComponent: StyledText { + text: "/" + color: Colours.palette.m3onSurfaceVariant + font.bold: true + } + } + + Item { + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + + Loader { + anchors.fill: parent + active: folder.index < root.dialog.cwd.length - 1 + sourceComponent: StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); + } + } + } + + Loader { + id: homeIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + + active: folder.index === 0 && folder.modelData === "Home" + sourceComponent: MaterialIcon { + text: "home" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + fill: 1 + } + } + + StyledText { + id: folderName + + anchors.left: homeIcon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + + text: folder.modelData + color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface + font.bold: true + } + } + } + } + + Item { + Layout.fillWidth: true + } + } + } + } +} diff --git a/.github/components/filedialog/Sidebar.qml b/.github/components/filedialog/Sidebar.qml new file mode 100644 index 000000000..b55d7b379 --- /dev/null +++ b/.github/components/filedialog/Sidebar.qml @@ -0,0 +1,113 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: Sizes.sidebarWidth + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: inner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.padding.small / 2 + Layout.bottomMargin: Appearance.spacing.normal + text: qsTr("Files") + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.larger + font.bold: true + } + + Repeater { + model: ["Home", "Downloads", "Desktop", "Documents", "Music", "Pictures", "Videos"] + + StyledRect { + id: place + + required property string modelData + readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] + + Layout.fillWidth: true + implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) + + StateLayer { + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + + function onClicked(): void { + if (place.modelData === "Home") + root.dialog.cwd = ["Home"]; + else + root.dialog.cwd = ["Home", place.modelData]; + } + } + + RowLayout { + id: placeInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: { + const p = place.modelData; + if (p === "Home") + return "home"; + if (p === "Downloads") + return "file_download"; + if (p === "Desktop") + return "desktop_windows"; + if (p === "Documents") + return "description"; + if (p === "Music") + return "music_note"; + if (p === "Pictures") + return "image"; + if (p === "Videos") + return "video_library"; + return "folder"; + } + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: place.selected ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + Layout.fillWidth: true + text: place.modelData + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + } + } + } + } +} diff --git a/.github/components/filedialog/Sizes.qml b/.github/components/filedialog/Sizes.qml new file mode 100644 index 000000000..2ad31f9f6 --- /dev/null +++ b/.github/components/filedialog/Sizes.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell + +Singleton { + property int itemWidth: 103 + property int sidebarWidth: 200 +} diff --git a/.github/components/images/CachingIconImage.qml b/.github/components/images/CachingIconImage.qml new file mode 100644 index 000000000..1acc6a181 --- /dev/null +++ b/.github/components/images/CachingIconImage.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import qs.utils +import Quickshell.Widgets +import QtQuick + +Item { + id: root + + readonly property int status: loader.item?.status ?? Image.Null + readonly property real actualSize: Math.min(width, height) + property real implicitSize + property url source + + implicitWidth: implicitSize + implicitHeight: implicitSize + + Loader { + id: loader + + anchors.fill: parent + sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null + } + + Component { + id: cachingImage + + CachingImage { + path: Paths.toLocalFile(root.source) + fillMode: Image.PreserveAspectFit + } + } + + Component { + id: iconImage + + IconImage { + source: root.source + asynchronous: true + } + } +} diff --git a/.github/components/images/CachingImage.qml b/.github/components/images/CachingImage.qml new file mode 100644 index 000000000..e8f957a7d --- /dev/null +++ b/.github/components/images/CachingImage.qml @@ -0,0 +1,28 @@ +import qs.utils +import Caelestia.Internal +import Quickshell +import QtQuick + +Image { + id: root + + property alias path: manager.path + + asynchronous: true + fillMode: Image.PreserveAspectCrop + + Connections { + target: QsWindow.window + + function onDevicePixelRatioChanged(): void { + manager.updateSource(); + } + } + + CachingImageManager { + id: manager + + item: root + cacheDir: Qt.resolvedUrl(Paths.imagecache) + } +} diff --git a/.github/components/misc/CustomShortcut.qml b/.github/components/misc/CustomShortcut.qml new file mode 100644 index 000000000..aa35ed8f5 --- /dev/null +++ b/.github/components/misc/CustomShortcut.qml @@ -0,0 +1,5 @@ +import Quickshell.Hyprland + +GlobalShortcut { + appid: "caelestia" +} diff --git a/.github/components/misc/Ref.qml b/.github/components/misc/Ref.qml new file mode 100644 index 000000000..0a694a472 --- /dev/null +++ b/.github/components/misc/Ref.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + required property var service + + Component.onCompleted: service.refCount++ + Component.onDestruction: service.refCount-- +} diff --git a/.github/components/widgets/ExtraIndicator.qml b/.github/components/widgets/ExtraIndicator.qml new file mode 100644 index 000000000..db73ea08f --- /dev/null +++ b/.github/components/widgets/ExtraIndicator.qml @@ -0,0 +1,51 @@ +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick + +StyledRect { + required property int extra + + anchors.right: parent.right + anchors.margins: Appearance.padding.normal + + color: Colours.palette.m3tertiary + radius: Appearance.rounding.small + + implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: count.implicitHeight + Appearance.padding.small * 2 + + opacity: extra > 0 ? 1 : 0 + scale: extra > 0 ? 1 : 0.5 + + Elevation { + anchors.fill: parent + radius: parent.radius + opacity: parent.opacity + z: -1 + level: 2 + } + + StyledText { + id: count + + anchors.centerIn: parent + animate: parent.opacity > 0 + text: qsTr("+%1").arg(parent.extra) + color: Colours.palette.m3onTertiary + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + } + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } +} diff --git a/.github/config/Appearance.qml b/.github/config/Appearance.qml new file mode 100644 index 000000000..241c21a78 --- /dev/null +++ b/.github/config/Appearance.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import Quickshell + +Singleton { + // Literally just here to shorten accessing stuff :woe: + // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` + readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding + readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing + readonly property AppearanceConfig.Padding padding: Config.appearance.padding + readonly property AppearanceConfig.FontStuff font: Config.appearance.font + readonly property AppearanceConfig.Anim anim: Config.appearance.anim + readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency +} diff --git a/.github/config/AppearanceConfig.qml b/.github/config/AppearanceConfig.qml new file mode 100644 index 000000000..3d590dca2 --- /dev/null +++ b/.github/config/AppearanceConfig.qml @@ -0,0 +1,94 @@ +import Quickshell.Io + +JsonObject { + property Rounding rounding: Rounding {} + property Spacing spacing: Spacing {} + property Padding padding: Padding {} + property FontStuff font: FontStuff {} + property Anim anim: Anim {} + property Transparency transparency: Transparency {} + + component Rounding: JsonObject { + property real scale: 1 + property int small: 12 * scale + property int normal: 17 * scale + property int large: 25 * scale + property int full: 1000 * scale + } + + component Spacing: JsonObject { + property real scale: 1 + property int small: 7 * scale + property int smaller: 10 * scale + property int normal: 12 * scale + property int larger: 15 * scale + property int large: 20 * scale + } + + component Padding: JsonObject { + property real scale: 1 + property int small: 5 * scale + property int smaller: 7 * scale + property int normal: 10 * scale + property int larger: 12 * scale + property int large: 15 * scale + } + + component FontFamily: JsonObject { + property string sans: "Rubik" + property string mono: "CaskaydiaCove NF" + property string material: "Material Symbols Rounded" + property string clock: "Rubik" + } + + component FontSize: JsonObject { + property real scale: 1 + property int small: 11 * scale + property int smaller: 12 * scale + property int normal: 13 * scale + property int larger: 15 * scale + property int large: 18 * scale + property int extraLarge: 28 * scale + } + + component FontStuff: JsonObject { + property FontFamily family: FontFamily {} + property FontSize size: FontSize {} + } + + component AnimCurves: JsonObject { + property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + property list standard: [0.2, 0, 0, 1, 1, 1] + property list standardAccel: [0.3, 0, 1, 1, 1, 1] + property list standardDecel: [0, 0, 0, 1, 1, 1] + property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + } + + component AnimDurations: JsonObject { + property real scale: 1 + property int small: 200 * scale + property int normal: 400 * scale + property int large: 600 * scale + property int extraLarge: 1000 * scale + property int expressiveFastSpatial: 350 * scale + property int expressiveDefaultSpatial: 500 * scale + property int expressiveEffects: 200 * scale + } + + component Anim: JsonObject { + property real mediaGifSpeedAdjustment: 300 + property real sessionGifSpeed: 0.7 + property AnimCurves curves: AnimCurves {} + property AnimDurations durations: AnimDurations {} + } + + component Transparency: JsonObject { + property bool enabled: false + property real base: 0.85 + property real layers: 0.4 + } +} diff --git a/.github/config/BackgroundConfig.qml b/.github/config/BackgroundConfig.qml new file mode 100644 index 000000000..8383f5248 --- /dev/null +++ b/.github/config/BackgroundConfig.qml @@ -0,0 +1,37 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool wallpaperEnabled: true + property DesktopClock desktopClock: DesktopClock {} + property Visualiser visualiser: Visualiser {} + + component DesktopClock: JsonObject { + property bool enabled: false + property real scale: 1.0 + property string position: "bottom-right" + property bool invertColors: false + property DesktopClockBackground background: DesktopClockBackground {} + property DesktopClockShadow shadow: DesktopClockShadow {} + } + + component DesktopClockBackground: JsonObject { + property bool enabled: false + property real opacity: 0.7 + property bool blur: true + } + + component DesktopClockShadow: JsonObject { + property bool enabled: true + property real opacity: 0.7 + property real blur: 0.4 + } + + component Visualiser: JsonObject { + property bool enabled: false + property bool autoHide: true + property bool blur: false + property real rounding: 1 + property real spacing: 1 + } +} diff --git a/.github/config/BarConfig.qml b/.github/config/BarConfig.qml new file mode 100644 index 000000000..310344b11 --- /dev/null +++ b/.github/config/BarConfig.qml @@ -0,0 +1,127 @@ +import Quickshell.Io + +JsonObject { + property bool persistent: true + property bool showOnHover: true + property int dragThreshold: 20 + property ScrollActions scrollActions: ScrollActions {} + property Popouts popouts: Popouts {} + property Workspaces workspaces: Workspaces {} + property ActiveWindow activeWindow: ActiveWindow {} + property Tray tray: Tray {} + property Status status: Status {} + property Clock clock: Clock {} + property Sizes sizes: Sizes {} + property list excludedScreens: [] + + property list entries: [ + { + id: "logo", + enabled: true + }, + { + id: "workspaces", + enabled: true + }, + { + id: "spacer", + enabled: true + }, + { + id: "activeWindow", + enabled: true + }, + { + id: "spacer", + enabled: true + }, + { + id: "tray", + enabled: true + }, + { + id: "clock", + enabled: true + }, + { + id: "statusIcons", + enabled: true + }, + { + id: "power", + enabled: true + } + ] + + component ScrollActions: JsonObject { + property bool workspaces: true + property bool volume: true + property bool brightness: true + } + + component Popouts: JsonObject { + property bool activeWindow: true + property bool tray: true + property bool statusIcons: true + } + + component Workspaces: JsonObject { + property int shown: 5 + property bool activeIndicator: true + property bool occupiedBg: false + property bool showWindows: true + property bool showWindowsOnSpecialWorkspaces: showWindows + property int maxWindowIcons: 0 // 0 = unlimited + property bool activeTrail: false + property bool perMonitorWorkspaces: true + property string label: " " // if empty, will show workspace name's first letter + property string occupiedLabel: "󰮯" + property string activeLabel: "󰮯" + property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty + property list specialWorkspaceIcons: [] + property list windowIcons: [ + { + regex: "steam(_app_(default|[0-9]+))?", + icon: "sports_esports" + } + ] + } + + component ActiveWindow: JsonObject { + property bool compact: false + property bool inverted: false + property bool showOnHover: true + } + + component Tray: JsonObject { + property bool background: false + property bool recolour: false + property bool compact: false + property list iconSubs: [] + property list hiddenIcons: [] + } + + component Status: JsonObject { + property bool showAudio: false + property bool showMicrophone: false + property bool showKbLayout: false + property bool showNetwork: true + property bool showWifi: true + property bool showBluetooth: true + property bool showBattery: true + property bool showLockStatus: true + } + + component Clock: JsonObject { + property bool showIcon: true + } + + component Sizes: JsonObject { + property int innerWidth: 40 + property int windowPreviewSize: 400 + property int trayMenuWidth: 300 + property int batteryWidth: 250 + property int networkWidth: 320 + property int kbLayoutWidth: 320 + } +} diff --git a/.github/config/BorderConfig.qml b/.github/config/BorderConfig.qml new file mode 100644 index 000000000..b15811fdd --- /dev/null +++ b/.github/config/BorderConfig.qml @@ -0,0 +1,6 @@ +import Quickshell.Io + +JsonObject { + property int thickness: Appearance.padding.normal + property int rounding: Appearance.rounding.large +} diff --git a/.github/config/Config.qml b/.github/config/Config.qml new file mode 100644 index 000000000..fa7ca4998 --- /dev/null +++ b/.github/config/Config.qml @@ -0,0 +1,476 @@ +pragma Singleton + +import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property alias appearance: adapter.appearance + property alias general: adapter.general + property alias background: adapter.background + property alias bar: adapter.bar + property alias border: adapter.border + property alias dashboard: adapter.dashboard + property alias controlCenter: adapter.controlCenter + property alias launcher: adapter.launcher + property alias notifs: adapter.notifs + property alias osd: adapter.osd + property alias session: adapter.session + property alias winfo: adapter.winfo + property alias lock: adapter.lock + property alias utilities: adapter.utilities + property alias sidebar: adapter.sidebar + property alias services: adapter.services + property alias paths: adapter.paths + + property bool recentlySaved: false + + // Public save function - call this to persist config changes + function save(): void { + saveTimer.restart(); + recentlySaved = true; + recentSaveCooldown.restart(); + } + + // Helper function to serialize the config object + function serializeConfig(): var { + return { + appearance: serializeAppearance(), + general: serializeGeneral(), + background: serializeBackground(), + bar: serializeBar(), + border: serializeBorder(), + dashboard: serializeDashboard(), + controlCenter: serializeControlCenter(), + launcher: serializeLauncher(), + notifs: serializeNotifs(), + osd: serializeOsd(), + session: serializeSession(), + winfo: serializeWinfo(), + lock: serializeLock(), + utilities: serializeUtilities(), + sidebar: serializeSidebar(), + services: serializeServices(), + paths: serializePaths() + }; + } + + function serializeAppearance(): var { + return { + rounding: { + scale: appearance.rounding.scale + }, + spacing: { + scale: appearance.spacing.scale + }, + padding: { + scale: appearance.padding.scale + }, + font: { + family: { + sans: appearance.font.family.sans, + mono: appearance.font.family.mono, + material: appearance.font.family.material, + clock: appearance.font.family.clock + }, + size: { + scale: appearance.font.size.scale + } + }, + anim: { + mediaGifSpeedAdjustment: 300, + sessionGifSpeed: 0.7, + durations: { + scale: appearance.anim.durations.scale + } + }, + transparency: { + enabled: appearance.transparency.enabled, + base: appearance.transparency.base, + layers: appearance.transparency.layers + } + }; + } + + function serializeGeneral(): var { + return { + logo: general.logo, + excludedScreens: general.excludedScreens, + apps: { + terminal: general.apps.terminal, + audio: general.apps.audio, + playback: general.apps.playback, + explorer: general.apps.explorer + }, + idle: { + lockBeforeSleep: general.idle.lockBeforeSleep, + inhibitWhenAudio: general.idle.inhibitWhenAudio, + timeouts: general.idle.timeouts + }, + battery: { + warnLevels: general.battery.warnLevels, + criticalLevel: general.battery.criticalLevel + } + }; + } + + function serializeBackground(): var { + return { + enabled: background.enabled, + wallpaperEnabled: background.wallpaperEnabled, + desktopClock: { + enabled: background.desktopClock.enabled, + scale: background.desktopClock.scale, + position: background.desktopClock.position, + invertColors: background.desktopClock.invertColors, + background: { + enabled: background.desktopClock.background.enabled, + opacity: background.desktopClock.background.opacity, + blur: background.desktopClock.background.blur + }, + shadow: { + enabled: background.desktopClock.shadow.enabled, + opacity: background.desktopClock.shadow.opacity, + blur: background.desktopClock.shadow.blur + } + }, + visualiser: { + enabled: background.visualiser.enabled, + autoHide: background.visualiser.autoHide, + blur: background.visualiser.blur, + rounding: background.visualiser.rounding, + spacing: background.visualiser.spacing + } + }; + } + + function serializeBar(): var { + return { + persistent: bar.persistent, + showOnHover: bar.showOnHover, + dragThreshold: bar.dragThreshold, + scrollActions: { + workspaces: bar.scrollActions.workspaces, + volume: bar.scrollActions.volume, + brightness: bar.scrollActions.brightness + }, + popouts: { + activeWindow: bar.popouts.activeWindow, + tray: bar.popouts.tray, + statusIcons: bar.popouts.statusIcons + }, + workspaces: { + shown: bar.workspaces.shown, + activeIndicator: bar.workspaces.activeIndicator, + occupiedBg: bar.workspaces.occupiedBg, + showWindows: bar.workspaces.showWindows, + showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, + maxWindowIcons: bar.workspaces.maxWindowIcons, + activeTrail: bar.workspaces.activeTrail, + perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, + label: bar.workspaces.label, + occupiedLabel: bar.workspaces.occupiedLabel, + activeLabel: bar.workspaces.activeLabel, + capitalisation: bar.workspaces.capitalisation, + specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons, + windowIcons: bar.workspaces.windowIcons + }, + activeWindow: { + compact: bar.activeWindow.compact, + inverted: bar.activeWindow.inverted, + showOnHover: bar.activeWindow.showOnHover + }, + tray: { + background: bar.tray.background, + recolour: bar.tray.recolour, + compact: bar.tray.compact, + iconSubs: bar.tray.iconSubs, + hiddenIcons: bar.tray.hiddenIcons + }, + status: { + showAudio: bar.status.showAudio, + showMicrophone: bar.status.showMicrophone, + showKbLayout: bar.status.showKbLayout, + showNetwork: bar.status.showNetwork, + showWifi: bar.status.showWifi, + showBluetooth: bar.status.showBluetooth, + showBattery: bar.status.showBattery, + showLockStatus: bar.status.showLockStatus + }, + clock: { + showIcon: bar.clock.showIcon + }, + entries: bar.entries, + excludedScreens: bar.excludedScreens + }; + } + + function serializeBorder(): var { + return { + thickness: border.thickness, + rounding: border.rounding + }; + } + + function serializeDashboard(): var { + return { + enabled: dashboard.enabled, + showOnHover: dashboard.showOnHover, + mediaUpdateInterval: dashboard.mediaUpdateInterval, + resourceUpdateInterval: dashboard.resourceUpdateInterval, + dragThreshold: dashboard.dragThreshold, + performance: { + showBattery: dashboard.performance.showBattery, + showGpu: dashboard.performance.showGpu, + showCpu: dashboard.performance.showCpu, + showMemory: dashboard.performance.showMemory, + showStorage: dashboard.performance.showStorage, + showNetwork: dashboard.performance.showNetwork + } + }; + } + + function serializeControlCenter(): var { + return {}; + } + + function serializeLauncher(): var { + return { + enabled: launcher.enabled, + showOnHover: launcher.showOnHover, + enableCategories: launcher.enableCategories ?? true, + maxShown: launcher.maxShown, + maxWallpapers: launcher.maxWallpapers, + specialPrefix: launcher.specialPrefix, + actionPrefix: launcher.actionPrefix, + enableDangerousActions: launcher.enableDangerousActions, + dragThreshold: launcher.dragThreshold, + vimKeybinds: launcher.vimKeybinds, + favouriteApps: launcher.favouriteApps, + hiddenApps: launcher.hiddenApps, + categories: launcher.categories, + useFuzzy: { + apps: launcher.useFuzzy.apps, + actions: launcher.useFuzzy.actions, + schemes: launcher.useFuzzy.schemes, + variants: launcher.useFuzzy.variants, + wallpapers: launcher.useFuzzy.wallpapers + }, + contextMenuMain: launcher.contextMenuMain, + contextMenuAdvanced: launcher.contextMenuAdvanced, + actions: launcher.actions + }; + } + + function serializeNotifs(): var { + return { + expire: notifs.expire, + defaultExpireTimeout: notifs.defaultExpireTimeout, + clearThreshold: notifs.clearThreshold, + expandThreshold: notifs.expandThreshold, + actionOnClick: notifs.actionOnClick, + groupPreviewNum: notifs.groupPreviewNum + }; + } + + function serializeOsd(): var { + return { + enabled: osd.enabled, + hideDelay: osd.hideDelay, + enableBrightness: osd.enableBrightness, + enableMicrophone: osd.enableMicrophone + }; + } + + function serializeSession(): var { + return { + enabled: session.enabled, + dragThreshold: session.dragThreshold, + vimKeybinds: session.vimKeybinds, + icons: { + logout: session.icons.logout, + shutdown: session.icons.shutdown, + hibernate: session.icons.hibernate, + reboot: session.icons.reboot + }, + commands: { + logout: session.commands.logout, + shutdown: session.commands.shutdown, + hibernate: session.commands.hibernate, + reboot: session.commands.reboot + } + }; + } + + function serializeWinfo(): var { + return {}; + } + + function serializeLock(): var { + return { + recolourLogo: lock.recolourLogo, + enableFprint: lock.enableFprint, + maxFprintTries: lock.maxFprintTries, + hideNotifs: lock.hideNotifs + }; + } + + function serializeUtilities(): var { + return { + enabled: utilities.enabled, + maxToasts: utilities.maxToasts, + toasts: { + configLoaded: utilities.toasts.configLoaded, + chargingChanged: utilities.toasts.chargingChanged, + gameModeChanged: utilities.toasts.gameModeChanged, + dndChanged: utilities.toasts.dndChanged, + audioOutputChanged: utilities.toasts.audioOutputChanged, + audioInputChanged: utilities.toasts.audioInputChanged, + capsLockChanged: utilities.toasts.capsLockChanged, + numLockChanged: utilities.toasts.numLockChanged, + kbLayoutChanged: utilities.toasts.kbLayoutChanged, + vpnChanged: utilities.toasts.vpnChanged, + nowPlaying: utilities.toasts.nowPlaying + }, + vpn: { + enabled: utilities.vpn.enabled, + provider: utilities.vpn.provider + }, + quickToggles: utilities.quickToggles + }; + } + + function serializeSidebar(): var { + return { + enabled: sidebar.enabled, + dragThreshold: sidebar.dragThreshold + }; + } + + function serializeServices(): var { + return { + weatherLocation: services.weatherLocation, + useFahrenheit: services.useFahrenheit, + useFahrenheitPerformance: services.useFahrenheitPerformance, + useTwelveHourClock: services.useTwelveHourClock, + gpuType: services.gpuType, + visualiserBars: services.visualiserBars, + audioIncrement: services.audioIncrement, + brightnessIncrement: services.brightnessIncrement, + maxVolume: services.maxVolume, + smartScheme: services.smartScheme, + defaultPlayer: services.defaultPlayer, + playerAliases: services.playerAliases + }; + } + + function serializePaths(): var { + return { + wallpaperDir: paths.wallpaperDir, + sessionGif: paths.sessionGif, + mediaGif: paths.mediaGif + }; + } + + ElapsedTimer { + id: timer + } + + Timer { + id: saveTimer + + interval: 500 + onTriggered: { + timer.restart(); + try { + // Parse current config to preserve structure and comments if possible + let config = {}; + try { + config = JSON.parse(fileView.text()); + } catch (e) { + // If parsing fails, start with empty object + config = {}; + } + + // Update config with current values + config = root.serializeConfig(); + + // Save to file with pretty printing + fileView.setText(JSON.stringify(config, null, 2)); + } catch (e) { + Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); + } + } + } + + Timer { + id: recentSaveCooldown + + interval: 2000 + onTriggered: { + root.recentlySaved = false; + } + } + + FileView { + id: fileView + + path: `${Paths.config}/shell.json` + watchChanges: true + onFileChanged: { + // Prevent reload loop - don't reload if we just saved + if (!root.recentlySaved) { + timer.restart(); + reload(); + } else { + // Self-initiated save - reload without toast + reload(); + } + } + onLoaded: { + try { + JSON.parse(text()); + const elapsed = timer.elapsedMs(); + // Only show toast for external changes (not our own saves) and when elapsed time is meaningful + if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); + } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); + } + } catch (e) { + Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); + } + } + onLoadFailed: err => { + if (err !== FileViewError.FileNotFound) + Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); + } + onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) + + JsonAdapter { + id: adapter + + property AppearanceConfig appearance: AppearanceConfig {} + property GeneralConfig general: GeneralConfig {} + property BackgroundConfig background: BackgroundConfig {} + property BarConfig bar: BarConfig {} + property BorderConfig border: BorderConfig {} + property DashboardConfig dashboard: DashboardConfig {} + property ControlCenterConfig controlCenter: ControlCenterConfig {} + property LauncherConfig launcher: LauncherConfig {} + property NotifsConfig notifs: NotifsConfig {} + property OsdConfig osd: OsdConfig {} + property SessionConfig session: SessionConfig {} + property WInfoConfig winfo: WInfoConfig {} + property LockConfig lock: LockConfig {} + property UtilitiesConfig utilities: UtilitiesConfig {} + property SidebarConfig sidebar: SidebarConfig {} + property ServiceConfig services: ServiceConfig {} + property UserPaths paths: UserPaths {} + } + } +} diff --git a/.github/config/ControlCenterConfig.qml b/.github/config/ControlCenterConfig.qml new file mode 100644 index 000000000..a5889491d --- /dev/null +++ b/.github/config/ControlCenterConfig.qml @@ -0,0 +1,10 @@ +import Quickshell.Io + +JsonObject { + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real ratio: 16 / 9 + } +} diff --git a/.github/config/DashboardConfig.qml b/.github/config/DashboardConfig.qml new file mode 100644 index 000000000..0a16cc1f9 --- /dev/null +++ b/.github/config/DashboardConfig.qml @@ -0,0 +1,40 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool showOnHover: true + property int mediaUpdateInterval: 500 + property int resourceUpdateInterval: 1000 + property int dragThreshold: 50 + property bool showDashboard: true + property bool showMedia: true + property bool showPerformance: true + property bool showWeather: true + property Sizes sizes: Sizes {} + property Performance performance: Performance {} + + component Performance: JsonObject { + property bool showBattery: true + property bool showGpu: true + property bool showCpu: true + property bool showMemory: true + property bool showStorage: true + property bool showNetwork: true + } + + component Sizes: JsonObject { + readonly property int tabIndicatorHeight: 3 + readonly property int tabIndicatorSpacing: 5 + readonly property int infoWidth: 200 + readonly property int infoIconSize: 25 + readonly property int dateTimeWidth: 110 + readonly property int mediaWidth: 200 + readonly property int mediaProgressSweep: 180 + readonly property int mediaProgressThickness: 8 + readonly property int resourceProgessThickness: 10 + readonly property int weatherWidth: 250 + readonly property int mediaCoverArtSize: 150 + readonly property int mediaVisualiserSize: 80 + readonly property int resourceSize: 200 + } +} diff --git a/.github/config/GeneralConfig.qml b/.github/config/GeneralConfig.qml new file mode 100644 index 000000000..0d1d1e191 --- /dev/null +++ b/.github/config/GeneralConfig.qml @@ -0,0 +1,61 @@ +import Quickshell.Io + +JsonObject { + property string logo: "" + property list excludedScreens: [] + property Apps apps: Apps {} + property Idle idle: Idle {} + property Battery battery: Battery {} + + component Apps: JsonObject { + property list terminal: ["foot"] + property list audio: ["pavucontrol"] + property list playback: ["mpv"] + property list explorer: ["thunar"] + } + + component Idle: JsonObject { + property bool lockBeforeSleep: true + property bool inhibitWhenAudio: true + property list timeouts: [ + { + timeout: 180, + idleAction: "lock" + }, + { + timeout: 300, + idleAction: "dpms off", + returnAction: "dpms on" + }, + { + timeout: 600, + idleAction: ["systemctl", "suspend-then-hibernate"] + } + ] + } + + component Battery: JsonObject { + property list warnLevels: [ + { + level: 20, + title: qsTr("Low battery"), + message: qsTr("You might want to plug in a charger"), + icon: "battery_android_frame_2" + }, + { + level: 10, + title: qsTr("Did you see the previous message?"), + message: qsTr("You should probably plug in a charger now"), + icon: "battery_android_frame_1" + }, + { + level: 5, + title: qsTr("Critical battery level"), + message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), + icon: "battery_android_alert", + critical: true + }, + ] + property int criticalLevel: 3 + } +} diff --git a/.github/config/LauncherConfig.qml b/.github/config/LauncherConfig.qml new file mode 100644 index 000000000..283fb7c58 --- /dev/null +++ b/.github/config/LauncherConfig.qml @@ -0,0 +1,159 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool showOnHover: false + property bool enableCategories: true + property int maxShown: 7 + property int maxWallpapers: 9 // Warning: even numbers look bad + property string specialPrefix: "@" + property string actionPrefix: ">" + property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout + property int dragThreshold: 50 + property bool vimKeybinds: false + property list favouriteApps: [] + property list hiddenApps: [] + property list contextMenuMain: [] + property list contextMenuAdvanced: [] + property list categories: [ + { name: "Development", icon: "code", apps: [] }, + { name: "Graphics", icon: "palette", apps: [] }, + { name: "Communication", icon: "chat", apps: [] }, + { name: "Media", icon: "play_circle", apps: [] }, + { name: "Office", icon: "description", apps: [] }, + { name: "Games", icon: "sports_esports", apps: [] }, + { name: "Utilities", icon: "build", apps: [] } + ] + property UseFuzzy useFuzzy: UseFuzzy {} + property Sizes sizes: Sizes {} + + component UseFuzzy: JsonObject { + property bool apps: false + property bool actions: false + property bool schemes: false + property bool variants: false + property bool wallpapers: false + } + + component Sizes: JsonObject { + property int itemWidth: 600 + property int itemHeight: 57 + property int wallpaperWidth: 280 + property int wallpaperHeight: 200 + } + + property list actions: [ + { + name: "Calculator", + icon: "calculate", + description: "Do simple math equations (powered by Qalc)", + command: ["autocomplete", "calc"], + enabled: true, + dangerous: false + }, + { + name: "Scheme", + icon: "palette", + description: "Change the current colour scheme", + command: ["autocomplete", "scheme"], + enabled: true, + dangerous: false + }, + { + name: "Wallpaper", + icon: "image", + description: "Change the current wallpaper", + command: ["autocomplete", "wallpaper"], + enabled: true, + dangerous: false + }, + { + name: "Variant", + icon: "colors", + description: "Change the current scheme variant", + command: ["autocomplete", "variant"], + enabled: true, + dangerous: false + }, + { + name: "Transparency", + icon: "opacity", + description: "Change shell transparency", + command: ["autocomplete", "transparency"], + enabled: false, + dangerous: false + }, + { + name: "Random", + icon: "casino", + description: "Switch to a random wallpaper", + command: ["caelestia", "wallpaper", "-r"], + enabled: true, + dangerous: false + }, + { + name: "Light", + icon: "light_mode", + description: "Change the scheme to light mode", + command: ["setMode", "light"], + enabled: true, + dangerous: false + }, + { + name: "Dark", + icon: "dark_mode", + description: "Change the scheme to dark mode", + command: ["setMode", "dark"], + enabled: true, + dangerous: false + }, + { + name: "Shutdown", + icon: "power_settings_new", + description: "Shutdown the system", + command: ["systemctl", "poweroff"], + enabled: true, + dangerous: true + }, + { + name: "Reboot", + icon: "cached", + description: "Reboot the system", + command: ["systemctl", "reboot"], + enabled: true, + dangerous: true + }, + { + name: "Logout", + icon: "exit_to_app", + description: "Log out of the current session", + command: ["loginctl", "terminate-user", ""], + enabled: true, + dangerous: true + }, + { + name: "Lock", + icon: "lock", + description: "Lock the current session", + command: ["loginctl", "lock-session"], + enabled: true, + dangerous: false + }, + { + name: "Sleep", + icon: "bedtime", + description: "Suspend then hibernate", + command: ["systemctl", "suspend-then-hibernate"], + enabled: true, + dangerous: false + }, + { + name: "Settings", + icon: "settings", + description: "Configure the shell", + command: ["caelestia", "shell", "controlCenter", "open"], + enabled: true, + dangerous: false + } + ] +} diff --git a/.github/config/LockConfig.qml b/.github/config/LockConfig.qml new file mode 100644 index 000000000..d0a9fb357 --- /dev/null +++ b/.github/config/LockConfig.qml @@ -0,0 +1,15 @@ +import Quickshell.Io + +JsonObject { + property bool recolourLogo: false + property bool enableFprint: true + property int maxFprintTries: 3 + property bool hideNotifs: false + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real ratio: 16 / 9 + property int centerWidth: 600 + } +} diff --git a/.github/config/NotifsConfig.qml b/.github/config/NotifsConfig.qml new file mode 100644 index 000000000..fa2db494e --- /dev/null +++ b/.github/config/NotifsConfig.qml @@ -0,0 +1,18 @@ +import Quickshell.Io + +JsonObject { + property bool expire: true + property int defaultExpireTimeout: 5000 + property real clearThreshold: 0.3 + property int expandThreshold: 20 + property bool actionOnClick: false + property int groupPreviewNum: 3 + property bool openExpanded: false // Show the notifichation in expanded state when opening + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int width: 400 + property int image: 41 + property int badge: 20 + } +} diff --git a/.github/config/OsdConfig.qml b/.github/config/OsdConfig.qml new file mode 100644 index 000000000..543fc41e5 --- /dev/null +++ b/.github/config/OsdConfig.qml @@ -0,0 +1,14 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int hideDelay: 2000 + property bool enableBrightness: true + property bool enableMicrophone: false + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int sliderWidth: 30 + property int sliderHeight: 150 + } +} diff --git a/.github/config/ServiceConfig.qml b/.github/config/ServiceConfig.qml new file mode 100644 index 000000000..29600cc54 --- /dev/null +++ b/.github/config/ServiceConfig.qml @@ -0,0 +1,22 @@ +import Quickshell.Io +import QtQuick + +JsonObject { + property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" + property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) + property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) + property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") + property string gpuType: "" + property int visualiserBars: 45 + property real audioIncrement: 0.1 + property real brightnessIncrement: 0.1 + property real maxVolume: 1.0 + property bool smartScheme: true + property string defaultPlayer: "Spotify" + property list playerAliases: [ + { + "from": "com.github.th_ch.youtube_music", + "to": "YT Music" + } + ] +} diff --git a/.github/config/SessionConfig.qml b/.github/config/SessionConfig.qml new file mode 100644 index 000000000..414f821a2 --- /dev/null +++ b/.github/config/SessionConfig.qml @@ -0,0 +1,29 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int dragThreshold: 30 + property bool vimKeybinds: false + property Icons icons: Icons {} + property Commands commands: Commands {} + + property Sizes sizes: Sizes {} + + component Icons: JsonObject { + property string logout: "logout" + property string shutdown: "power_settings_new" + property string hibernate: "downloading" + property string reboot: "cached" + } + + component Commands: JsonObject { + property list logout: ["loginctl", "terminate-user", ""] + property list shutdown: ["systemctl", "poweroff"] + property list hibernate: ["systemctl", "hibernate"] + property list reboot: ["systemctl", "reboot"] + } + + component Sizes: JsonObject { + property int button: 80 + } +} diff --git a/.github/config/SidebarConfig.qml b/.github/config/SidebarConfig.qml new file mode 100644 index 000000000..a871562b9 --- /dev/null +++ b/.github/config/SidebarConfig.qml @@ -0,0 +1,11 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int dragThreshold: 80 + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int width: 430 + } +} diff --git a/.github/config/UserPaths.qml b/.github/config/UserPaths.qml new file mode 100644 index 000000000..f8de26782 --- /dev/null +++ b/.github/config/UserPaths.qml @@ -0,0 +1,8 @@ +import qs.utils +import Quickshell.Io + +JsonObject { + property string wallpaperDir: `${Paths.pictures}/Wallpapers` + property string sessionGif: "root:/assets/kurukuru.gif" + property string mediaGif: "root:/assets/bongocat.gif" +} diff --git a/.github/config/UtilitiesConfig.qml b/.github/config/UtilitiesConfig.qml new file mode 100644 index 000000000..4d1dd6e8d --- /dev/null +++ b/.github/config/UtilitiesConfig.qml @@ -0,0 +1,66 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int maxToasts: 4 + + property Sizes sizes: Sizes {} + property Toasts toasts: Toasts {} + property Vpn vpn: Vpn {} + + component Sizes: JsonObject { + property int width: 430 + property int toastWidth: 430 + } + + component Toasts: JsonObject { + property bool configLoaded: true + property bool chargingChanged: true + property bool gameModeChanged: true + property bool dndChanged: true + property bool audioOutputChanged: true + property bool audioInputChanged: true + property bool capsLockChanged: true + property bool numLockChanged: true + property bool kbLayoutChanged: true + property bool kbLimit: true + property bool vpnChanged: true + property bool nowPlaying: false + } + + component Vpn: JsonObject { + property bool enabled: false + property list provider: ["netbird"] + } + + property list quickToggles: [ + { + id: "wifi", + enabled: true + }, + { + id: "bluetooth", + enabled: true + }, + { + id: "mic", + enabled: true + }, + { + id: "settings", + enabled: true + }, + { + id: "gameMode", + enabled: true + }, + { + id: "dnd", + enabled: true + }, + { + id: "vpn", + enabled: false + } + ] +} diff --git a/.github/config/WInfoConfig.qml b/.github/config/WInfoConfig.qml new file mode 100644 index 000000000..502578075 --- /dev/null +++ b/.github/config/WInfoConfig.qml @@ -0,0 +1,10 @@ +import Quickshell.Io + +JsonObject { + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real detailsWidth: 500 + } +} diff --git a/.github/extras/CMakeLists.txt b/.github/extras/CMakeLists.txt new file mode 100644 index 000000000..52fe17c54 --- /dev/null +++ b/.github/extras/CMakeLists.txt @@ -0,0 +1,9 @@ +# Version +add_executable(version version.cpp) +target_compile_definitions(version PRIVATE + PROJECT_NAME="${PROJECT_NAME}" + VERSION="${VERSION}" + GIT_REVISION="${GIT_REVISION}" + DISTRIBUTOR="${DISTRIBUTOR}" +) +install(TARGETS version DESTINATION ${INSTALL_LIBDIR}) diff --git a/.github/extras/version.cpp b/.github/extras/version.cpp new file mode 100644 index 000000000..d63434170 --- /dev/null +++ b/.github/extras/version.cpp @@ -0,0 +1,27 @@ +#include + +int main(int argc, char* argv[]) { + if (argc > 1) { + std::string arg = argv[1]; + + if (arg == "-t" || arg == "--terse") { + std::cout << PROJECT_NAME << std::endl; + std::cout << VERSION << std::endl; + std::cout << GIT_REVISION << std::endl; + std::cout << DISTRIBUTOR << std::endl; + } else if (arg == "-s" || arg == "--short") { + std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR << std::endl; + } else { + std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; + return arg != "-h" && arg != "--help"; + } + } else { + std::cout << "Project: " << PROJECT_NAME << std::endl; + std::cout << "Version: " << VERSION << std::endl; + std::cout << "Git revision: " << GIT_REVISION << std::endl; + std::cout << "Distributor: " << DISTRIBUTOR << std::endl; + } + + return 0; +} From 081d9e856a5642df67535648ef75747efc416d99 Mon Sep 17 00:00:00 2001 From: AleksElixir <71710534+AleksElixir@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:52:07 +0200 Subject: [PATCH 2/4] Add files via upload --- modules/areapicker/AreaPicker.qml | 18 +- modules/areapicker/Picker.qml | 13 +- modules/background/Background.qml | 21 +- modules/background/DesktopClock.qml | 8 +- modules/background/Visualiser.qml | 12 +- modules/background/Wallpaper.qml | 19 +- modules/bar/Bar.qml | 31 +- modules/bar/BarWrapper.qml | 17 +- modules/bar/components/ActiveWindow.qml | 7 +- modules/bar/components/Clock.qml | 71 +-- modules/bar/components/OsIcon.qml | 3 +- modules/bar/components/Power.qml | 14 +- modules/bar/components/Settings.qml | 18 +- modules/bar/components/SettingsIcon.qml | 18 +- modules/bar/components/StatusIcons.qml | 21 +- modules/bar/components/Tray.qml | 8 +- modules/bar/components/TrayItem.qml | 4 +- .../components/workspaces/ActiveIndicator.qml | 7 +- .../bar/components/workspaces/OccupiedBg.qml | 18 +- .../workspaces/SpecialWorkspaces.qml | 293 ++++++----- .../bar/components/workspaces/Workspace.qml | 15 +- .../bar/components/workspaces/Workspaces.qml | 18 +- modules/bar/popouts/ActiveWindow.qml | 21 +- modules/bar/popouts/Audio.qml | 14 +- modules/bar/popouts/Background.qml | 4 +- modules/bar/popouts/Battery.qml | 13 +- modules/bar/popouts/Bluetooth.qml | 42 +- modules/bar/popouts/Content.qml | 66 ++- modules/bar/popouts/LockStatus.qml | 2 +- modules/bar/popouts/Network.qml | 43 +- modules/bar/popouts/TrayMenu.qml | 51 +- modules/bar/popouts/WirelessPassword.qml | 222 ++++----- modules/bar/popouts/Wrapper.qml | 33 +- modules/bar/popouts/kblayout/KbLayout.qml | 51 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 177 ++++--- modules/controlcenter/ControlCenter.qml | 14 +- modules/controlcenter/NavRail.qml | 16 +- modules/controlcenter/Panes.qml | 36 +- modules/controlcenter/Session.qml | 2 +- modules/controlcenter/WindowFactory.qml | 12 +- modules/controlcenter/WindowTitle.qml | 8 +- .../appearance/AppearancePane.qml | 34 +- .../appearance/sections/AnimationsSection.qml | 6 +- .../appearance/sections/BackgroundSection.qml | 30 +- .../appearance/sections/BorderSection.qml | 8 +- .../sections/ColorSchemeSection.qml | 14 +- .../sections/ColorVariantSection.qml | 12 +- .../appearance/sections/FontsSection.qml | 73 ++- .../appearance/sections/ScalesSection.qml | 6 +- .../appearance/sections/ThemeModeSection.qml | 4 +- .../sections/TransparencySection.qml | 6 +- modules/controlcenter/audio/AudioPane.qml | 82 ++- modules/controlcenter/bluetooth/BtPane.qml | 9 +- modules/controlcenter/bluetooth/Details.qml | 23 +- .../controlcenter/bluetooth/DeviceList.qml | 16 +- modules/controlcenter/bluetooth/Settings.qml | 24 +- .../components/ConnectedButtonGroup.qml | 15 +- .../components/DeviceDetails.qml | 10 +- .../controlcenter/components/DeviceList.qml | 13 +- .../components/PaneTransition.qml | 2 +- .../components/ReadonlySlider.qml | 4 +- .../components/SettingsHeader.qml | 4 +- .../controlcenter/components/SliderInput.qml | 40 +- .../components/SplitPaneLayout.qml | 14 +- .../components/SplitPaneWithDetails.qml | 20 +- .../components/WallpaperGrid.qml | 37 +- .../controlcenter/dashboard/DashboardPane.qml | 19 +- .../dashboard/GeneralSection.qml | 4 +- .../dashboard/PerformanceSection.qml | 2 +- .../controlcenter/launcher/LauncherPane.qml | 465 +++++++++++++++--- modules/controlcenter/launcher/Settings.qml | 232 ++++++++- .../controlcenter/network/EthernetDetails.qml | 6 +- .../controlcenter/network/EthernetList.qml | 10 +- .../controlcenter/network/EthernetPane.qml | 4 +- .../network/EthernetSettings.qml | 4 +- .../controlcenter/network/NetworkSettings.qml | 8 +- .../controlcenter/network/NetworkingPane.qml | 35 +- modules/controlcenter/network/VpnDetails.qml | 18 +- modules/controlcenter/network/VpnList.qml | 153 +++--- modules/controlcenter/network/VpnSettings.qml | 13 +- .../controlcenter/network/WirelessDetails.qml | 116 +++-- .../controlcenter/network/WirelessList.qml | 23 +- .../controlcenter/network/WirelessPane.qml | 4 +- .../network/WirelessPasswordDialog.qml | 127 +++-- .../network/WirelessSettings.qml | 4 +- .../controlcenter/state/BluetoothState.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 77 +-- modules/controlcenter/vpn/VpnDetails.qml | 0 modules/controlcenter/vpn/VpnList.qml | 0 89 files changed, 1804 insertions(+), 1509 deletions(-) create mode 100644 modules/controlcenter/vpn/VpnDetails.qml create mode 100644 modules/controlcenter/vpn/VpnList.qml diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index 76cc10399..308b7d232 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import Quickshell -import Quickshell.Io -import Quickshell.Wayland import qs.components.containers import qs.components.misc import qs.services +import Quickshell +import Quickshell.Wayland +import Quickshell.Io Scope { LazyLoader { @@ -48,6 +48,8 @@ Scope { } IpcHandler { + target: "picker" + function open(): void { root.freeze = false; root.closing = false; @@ -75,13 +77,9 @@ Scope { root.clipboardOnly = true; root.activeAsync = true; } - - target: "picker" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshot" description: "Open screenshot tool" onPressed: { @@ -92,9 +90,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotFreeze" description: "Open screenshot tool (freeze mode)" onPressed: { @@ -105,9 +101,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotClip" description: "Open screenshot tool (clipboard)" onPressed: { @@ -118,9 +112,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotFreezeClip" description: "Open screenshot tool (freeze mode, clipboard)" onPressed: { diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index e6272737a..f4f4a3679 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Caelestia import qs.components import qs.services import qs.config +import Caelestia +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects MouseArea { id: root @@ -206,7 +206,6 @@ MouseArea { Loader { id: screencopy - asynchronous: true anchors.fill: parent active: root.loader.freeze diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 95109e121..c1f149a00 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Wayland +import qs.components import qs.components.containers import qs.services import qs.config +import Quickshell +import Quickshell.Wayland +import QtQuick Loader { - asynchronous: true active: Config.background.enabled sourceComponent: Variants { @@ -39,8 +39,6 @@ Loader { Loader { id: wallpaper - asynchronous: true - anchors.fill: parent active: Config.background.wallpaperEnabled @@ -56,8 +54,6 @@ Loader { Loader { id: clockLoader - - asynchronous: true active: Config.background.desktopClock.enabled anchors.margins: Appearance.padding.large * 2 @@ -67,7 +63,6 @@ Loader { states: [ State { name: "top-left" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -76,7 +71,6 @@ Loader { }, State { name: "top-center" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -85,7 +79,6 @@ Loader { }, State { name: "top-right" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -94,7 +87,6 @@ Loader { }, State { name: "middle-left" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -103,7 +95,6 @@ Loader { }, State { name: "middle-center" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -112,7 +103,6 @@ Loader { }, State { name: "middle-right" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -121,7 +111,6 @@ Loader { }, State { name: "bottom-left" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -130,7 +119,6 @@ Loader { }, State { name: "bottom-center" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -139,7 +127,6 @@ Loader { }, State { name: "bottom-right" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 86f9c623f..f9a06a2aa 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects Item { id: root @@ -40,7 +40,6 @@ Item { } Loader { - asynchronous: true anchors.fill: parent active: root.blurEnabled @@ -102,7 +101,6 @@ Item { } Loader { - asynchronous: true Layout.alignment: Qt.AlignTop Layout.topMargin: Appearance.padding.large * 1.4 * root.scale diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 780abdf4b..35a086b92 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Widgets -import Caelestia.Services import qs.components import qs.services import qs.config +import Caelestia.Services +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Effects Item { id: root @@ -21,7 +21,6 @@ Item { opacity: shouldBeActive ? 1 : 0 Loader { - asynchronous: true anchors.fill: parent active: root.opacity > 0 && Config.background.visualiser.blur @@ -43,7 +42,6 @@ Item { layer.enabled: true Loader { - asynchronous: true anchors.fill: parent anchors.topMargin: root.offset anchors.bottomMargin: -root.offset diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 10d743baa..39a48fc8f 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -1,19 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components -import qs.components.filedialog import qs.components.images +import qs.components.filedialog import qs.services import qs.config import qs.utils +import QtQuick Item { id: root property string source: Wallpapers.current property Image current: one - property bool completed onSourceChanged: { if (!source) @@ -26,17 +25,13 @@ Item { Component.onCompleted: { if (source) - Qt.callLater(() => { - one.update(); - completed = true; - }); + Qt.callLater(() => one.update()); } Loader { - asynchronous: true anchors.fill: parent - active: root.completed && !root.source + active: !root.source sourceComponent: StyledRect { color: Colours.palette.m3surfaceContainer @@ -79,12 +74,12 @@ Item { } StateLayer { + radius: parent.radius + color: Colours.palette.m3onPrimary + function onClicked(): void { dialog.open(); } - - radius: parent.radius - color: Colours.palette.m3onPrimary } StyledText { diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index b82a17c74..95c166e6f 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -1,20 +1,19 @@ pragma ComponentBehavior: Bound +import qs.services +import qs.config import "popouts" as BarPopouts import "components" import "components/workspaces" +import Quickshell import QtQuick import QtQuick.Layouts -import Quickshell -import qs.components -import qs.services -import qs.config ColumnLayout { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property BarPopouts.Wrapper popouts readonly property int vPadding: Appearance.padding.large @@ -23,9 +22,9 @@ ColumnLayout { return; for (let i = 0; i < repeater.count; i++) { - const loader = repeater.itemAt(i) as WrappedLoader; - if (loader?.enabled && loader.id === "tray") { - (loader.item as Tray).expanded = false; + const item = repeater.itemAt(i); + if (item?.enabled && item.id === "tray") { + item.item.expanded = false; } } } @@ -43,9 +42,11 @@ ColumnLayout { const id = ch.id; const top = ch.y; + const item = ch.item; + const itemHeight = item.implicitHeight; if (id === "statusIcons" && Config.bar.popouts.statusIcons) { - const items = (ch.item as StatusIcons).items; + const items = item.items; const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); if (icon) { popouts.currentName = icon.name; @@ -53,10 +54,9 @@ ColumnLayout { popouts.hasCurrent = true; } } else if (id === "tray" && Config.bar.popouts.tray) { - const tray = ch.item as Tray; - if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { - const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); - const trayItem = tray.items.itemAt(index); + if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { + const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); + const trayItem = item.items.itemAt(index); if (trayItem) { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); @@ -66,11 +66,11 @@ ColumnLayout { } } else { popouts.hasCurrent = false; - tray.expanded = true; + item.expanded = true; } } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); - popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; + popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; popouts.hasCurrent = true; } } @@ -194,7 +194,6 @@ ColumnLayout { return null; } - asynchronous: true Layout.alignment: Qt.AlignHCenter // Cursed ahh thing to add padding to first and last enabled components diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index df29f0af1..29961b62c 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -1,20 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts +import "popouts" as BarPopouts +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property BarPopouts.Wrapper popouts required property bool disabled - readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness @@ -22,15 +21,15 @@ Item { property bool isHovered function closeTray(): void { - (content.item as Bar)?.closeTray(); + content.item?.closeTray(); } function checkPopout(y: real): void { - (content.item as Bar)?.checkPopout(y); + content.item?.checkPopout(y); } function handleWheel(y: real, angleDelta: point): void { - (content.item as Bar)?.handleWheel(y, angleDelta); + content.item?.handleWheel(y, angleDelta); } visible: width > Config.border.thickness @@ -82,7 +81,7 @@ Item { width: root.contentWidth screen: root.screen visibilities: root.visibilities - popouts: root.popouts // qmllint disable incompatible-type + popouts: root.popouts } } } diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0fa277ee4..414c9c579 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import QtQuick Item { id: root @@ -19,7 +19,7 @@ Item { return qsTr("Desktop"); if (Config.bar.activeWindow.compact) { // " - " (standard hyphen), " — " (em dash), " – " (en dash) - const parts = title.split(/\s+[\-\u2013\u2014]\s+/); + const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); if (parts.length > 1) return parts[parts.length - 1].trim(); } @@ -39,7 +39,6 @@ Item { implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin Loader { - asynchronous: true anchors.fill: parent active: !Config.bar.activeWindow.showOnHover diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 90ed78cf1..801e93d77 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,71 +1,38 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.services import qs.config +import QtQuick -StyledRect { +Column { id: root - readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small - - implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: layout.implicitHeight + root.padding * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.full + property color colour: Colours.palette.m3tertiary - Column { - id: layout + spacing: Appearance.spacing.small - anchors.centerIn: parent - spacing: Appearance.spacing.small - - Loader { - asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - - active: Config.bar.clock.showIcon - visible: active - - sourceComponent: MaterialIcon { - text: "calendar_month" - color: root.colour - } - } + Loader { + anchors.horizontalCenter: parent.horizontalCenter - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + active: Config.bar.clock.showIcon + visible: active - visible: Config.bar.clock.showDate - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format("ddd\nd") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.sans + sourceComponent: MaterialIcon { + text: "calendar_month" color: root.colour } + } - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - visible: Config.bar.clock.showDate - height: visible ? 1 : 0 + StyledText { + id: text - width: parent.width * 0.8 - color: root.colour - opacity: 0.2 - } + anchors.horizontalCenter: parent.horizontalCenter - StyledText { - anchors.horizontalCenter: parent.horizontalCenter - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono - color: root.colour - } + horizontalAlignment: StyledText.AlignHCenter + text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") + font.pointSize: Appearance.font.size.smaller + font.family: Appearance.font.family.mono + color: root.colour } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index fc16a4fca..6710294a0 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,9 +1,9 @@ -import QtQuick import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import QtQuick Item { id: root @@ -21,7 +21,6 @@ Item { } Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 5f5738115..917bdf7fd 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,27 +1,29 @@ -import QtQuick import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - root.visibilities.session = !root.visibilities.session; - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + root.visibilities.session = !root.visibilities.session; + } } MaterialIcon { diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index f6ed88af6..5d562cef1 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -1,8 +1,9 @@ -import QtQuick import qs.components +import qs.modules.controlcenter import qs.services import qs.config -import qs.modules.controlcenter +import Quickshell +import QtQuick Item { id: root @@ -12,17 +13,18 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(null, { + active: "network" + }); + } } MaterialIcon { diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index f6ed88af6..5d562cef1 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -1,8 +1,9 @@ -import QtQuick import qs.components +import qs.modules.controlcenter import qs.services import qs.config -import qs.modules.controlcenter +import Quickshell +import QtQuick Item { id: root @@ -12,17 +13,18 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(null, { + active: "network" + }); + } } MaterialIcon { diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 455e04ef1..ca7dc2e3a 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth -import Quickshell.Services.UPower import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Services.UPower +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -178,9 +178,9 @@ StyledRect { MaterialIcon { animate: true text: { - if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type + if (!Bluetooth.defaultAdapter?.enabled) return "bluetooth_disabled"; - if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type + if (Bluetooth.devices.values.some(d => d.connected)) return "bluetooth_connected"; return "bluetooth"; } @@ -190,7 +190,7 @@ StyledRect { // Connected bluetooth devices Repeater { model: ScriptModel { - values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type + values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) } MaterialIcon { @@ -204,7 +204,7 @@ StyledRect { fill: 1 SequentialAnimation on opacity { - running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type + running: device.modelData?.state !== BluetoothDeviceState.Connected alwaysRunToEnd: true loops: Animation.Infinite @@ -264,7 +264,6 @@ StyledRect { component WrappedLoader: Loader { required property string name - asynchronous: true Layout.alignment: Qt.AlignHCenter visible: active } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 79b57755e..7bafda16f 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Services.SystemTray import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Services.SystemTray +import QtQuick StyledRect { id: root @@ -82,8 +82,6 @@ StyledRect { Loader { id: expandIcon - asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index c5cb9fe82..99119073d 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell.Services.SystemTray import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell.Services.SystemTray +import QtQuick MouseArea { id: root diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index e8a52d4d4..dae54b371 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,8 +1,8 @@ -import QtQuick import qs.components import qs.components.effects import qs.services import qs.config +import QtQuick StyledRect { id: root @@ -20,12 +20,13 @@ StyledRect { property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 - property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 + property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { - const ws = workspaces.itemAt(lastWs) as Workspace; + const ws = workspaces.itemAt(lastWs); + // console.log(ws, lastWs); return ws ? Math.min(ws.y + ws.size - offset, s) : 0; } return s; diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 3f0871603..56b215e67 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root @@ -52,8 +52,8 @@ Item { required property var modelData - readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type - readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type + readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null + readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null function getWsIdx(ws: int): int { let i = ws - 1; @@ -90,14 +90,14 @@ Item { } } + component Pill: QtObject { + property int start + property int end + } + Component { id: pillComp Pill {} } - - component Pill: QtObject { - property int start - property int end - } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index cef8a9906..cd3572be2 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) - readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" + readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { @@ -105,14 +105,147 @@ Item { highlightFollowsCurrentItem: false highlight: Item { y: view.currentItem?.y ?? 0 - implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + implicitHeight: view.currentItem?.size ?? 0 Behavior on y { Anim {} } } - delegate: SpecialWsDelegate {} + delegate: ColumnLayout { + id: ws + + required property HyprlandWorkspace modelData + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) + property int wsId + property string icon + property bool hasWindows + + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right + + spacing: 0 + + Component.onCompleted: { + wsId = modelData.id; + icon = Icons.getSpecialWsIcon(modelData.name); + hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; + } + + // Hacky thing cause modelData gets destroyed before the remove anim finishes + Connections { + target: ws.modelData + + function onIdChanged(): void { + if (ws.modelData) + ws.wsId = ws.modelData.id; + } + + function onNameChanged(): void { + if (ws.modelData) + ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); + } + + function onLastIpcObjectChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + } + + Connections { + target: Config.bar.workspaces + + function onShowWindowsOnSpecialWorkspacesChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + } + + Loader { + id: label + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + + sourceComponent: ws.icon.length === 1 ? letterComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + fill: 1 + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + + Component { + id: letterComp + + StyledText { + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + } + + Loader { + id: windows + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight + + visible: active + active: ws.hasWindows + + sourceComponent: Column { + spacing: 0 + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + + Repeater { + model: ScriptModel { + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); + const maxIcons = Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } + } + + MaterialIcon { + required property var modelData + + grade: 0 + text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on Layout.preferredHeight { + Anim {} + } + } + } add: Transition { Anim { @@ -160,7 +293,6 @@ Item { } Loader { - asynchronous: true active: Config.bar.workspaces.activeIndicator anchors.fill: parent @@ -172,7 +304,7 @@ Item { anchors.right: parent.right y: (view.currentItem?.y ?? 0) - view.contentY - implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + implicitHeight: view.currentItem?.size ?? 0 color: Colours.palette.m3tertiary radius: Appearance.rounding.full @@ -221,150 +353,11 @@ Item { if (Math.abs(event.y - startY) > drag.threshold) return; - const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; + const ws = view.itemAt(event.x, event.y); if (ws?.modelData) Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); else Hypr.dispatch("togglespecialworkspace special"); } } - - component SpecialWsDelegate: ColumnLayout { - id: ws - - required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) - property int wsId - property string icon - property bool hasWindows - - anchors.left: view.contentItem.left - anchors.right: view.contentItem.right - - spacing: 0 - - Component.onCompleted: { - wsId = modelData.id; - icon = Icons.getSpecialWsIcon(modelData.name); - hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; - } - - // Hacky thing cause modelData gets destroyed before the remove anim finishes - Connections { - function onIdChanged(): void { - if (ws.modelData) - ws.wsId = ws.modelData.id; - } - - function onNameChanged(): void { - if (ws.modelData) - ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); - } - - function onLastIpcObjectChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: ws.modelData - } - - Connections { - function onShowWindowsOnSpecialWorkspacesChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: Config.bar.workspaces - } - - Loader { - id: label - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 - - sourceComponent: ws.icon.length === 1 ? letterComp : iconComp - - Component { - id: iconComp - - MaterialIcon { - fill: 1 - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - - Component { - id: letterComp - - StyledText { - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - } - - Loader { - id: windows - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.preferredHeight: implicitHeight - - visible: active - active: ws.hasWindows - - sourceComponent: Column { - spacing: 0 - - add: Transition { - Anim { - properties: "scale" - from: 0 - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - - move: Transition { - Anim { - properties: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - properties: "x,y" - } - } - - Repeater { - model: ScriptModel { - values: { - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); - const maxIcons = Config.bar.workspaces.maxWindowIcons; - return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; - } - } - - MaterialIcon { - required property var modelData - - grade: 0 - text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") - color: Colours.palette.m3onSurfaceVariant - } - } - } - - Behavior on Layout.preferredHeight { - Anim {} - } - } - } } diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index bfbdc2ac1..f6e767ef9 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -1,12 +1,10 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -57,8 +55,6 @@ ColumnLayout { Loader { id: windows - asynchronous: true - Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.topMargin: -Config.bar.sizes.innerWidth / 10 @@ -92,8 +88,7 @@ ColumnLayout { Repeater { model: ScriptModel { values: { - const ws = root.ws; - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws); const maxIcons = Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index f205dfac0..b9fe87faf 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import qs.components import qs.services import qs.config +import qs.components +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects StyledClippingRect { id: root required property ShellScreen screen - readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" + readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property var occupied: { @@ -45,7 +45,6 @@ StyledClippingRect { } Loader { - asynchronous: true active: Config.bar.workspaces.occupiedBg anchors.fill: parent @@ -78,7 +77,6 @@ StyledClippingRect { } Loader { - asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.workspaces.activeIndicator @@ -92,7 +90,7 @@ StyledClippingRect { MouseArea { anchors.fill: layout onClicked: event => { - const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; + const ws = layout.childAt(event.x, event.y).ws; if (Hypr.activeWsId !== ws) Hypr.dispatch(`workspace ${ws}`); else @@ -112,8 +110,6 @@ StyledClippingRect { Loader { id: specialWs - asynchronous: true - anchors.fill: parent anchors.margins: Appearance.padding.small diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 7e4bd60a0..adf7b7740 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -1,16 +1,16 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell.Wayland -import Quickshell.Widgets import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell.Widgets +import Quickshell.Wayland +import QtQuick +import QtQuick.Layouts Item { id: root - required property PopoutState popouts + required property Item wrapper implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 implicitHeight: child.implicitHeight @@ -31,7 +31,6 @@ Item { IconImage { id: icon - asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: details.implicitHeight source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") @@ -65,11 +64,11 @@ Item { Layout.alignment: Qt.AlignVCenter StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { - root.popouts.detachRequested("winfo"); + root.wrapper.detach("winfo"); } - - radius: Appearance.rounding.normal } MaterialIcon { @@ -92,7 +91,7 @@ Item { ScreencopyView { id: preview - captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type + captureSource: Hypr.activeToplevel?.wayland ?? null live: visible constraintSize.width: Config.bar.sizes.windowPreviewSize diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 52ac3b806..58b29ba8d 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -1,18 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Services.Pipewire import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import Quickshell.Services.Pipewire +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import "../../controlcenter/network" Item { id: root - required property PopoutState popouts + required property var wrapper implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 @@ -112,7 +114,7 @@ Item { text: qsTr("Open settings") icon: "settings" - onClicked: root.popouts.detachRequested("audio") + onClicked: root.wrapper.detach("audio") } } } diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml index cfba86d3a..075b69881 100644 --- a/modules/bar/popouts/Background.qml +++ b/modules/bar/popouts/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 86d903d24..ac975e1b7 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell.Services.UPower import qs.components import qs.services import qs.config +import Quickshell.Services.UPower +import QtQuick Column { id: root @@ -37,12 +37,11 @@ Column { } Loader { - asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 + height: active ? (item?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 @@ -205,12 +204,12 @@ Column { implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 StateLayer { + radius: Appearance.rounding.full + color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { PowerProfiles.profile = parent.profile; } - - radius: Appearance.rounding.full - color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 2e87270a1..676da82f5 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -1,19 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts +import "../../controlcenter/network" ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper spacing: Appearance.spacing.small @@ -26,9 +27,9 @@ ColumnLayout { Toggle { label: qsTr("Enabled") - checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.enabled ?? false toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = checked; } @@ -36,9 +37,9 @@ ColumnLayout { Toggle { label: qsTr("Discovering") - checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.discovering ?? false toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discovering = checked; } @@ -48,7 +49,7 @@ ColumnLayout { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small text: { - const devices = Bluetooth.devices.values; // qmllint disable unresolved-type + const devices = Bluetooth.devices.values; let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) @@ -61,14 +62,14 @@ ColumnLayout { Repeater { model: ScriptModel { - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) } RowLayout { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type + readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small @@ -108,7 +109,7 @@ ColumnLayout { implicitHeight: connectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type + color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) CircularIndicator { anchors.fill: parent @@ -116,12 +117,12 @@ ColumnLayout { } StateLayer { + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: device.loading + function onClicked(): void { device.modelData.connected = !device.modelData.connected; } - - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type - disabled: device.loading } MaterialIcon { @@ -130,7 +131,7 @@ ColumnLayout { anchors.centerIn: parent animate: true text: device.modelData.connected ? "link_off" : "link" - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 @@ -141,18 +142,17 @@ ColumnLayout { } Loader { - asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth implicitHeight: connectBtn.implicitHeight StateLayer { + radius: Appearance.rounding.full + function onClicked(): void { device.modelData.forget(); } - - radius: Appearance.rounding.full } MaterialIcon { @@ -173,7 +173,7 @@ ColumnLayout { text: qsTr("Open settings") icon: "settings" - onClicked: root.popouts.detachRequested("bluetooth") + onClicked: root.wrapper.detach("bluetooth") } component Toggle: RowLayout { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 1c4792e1e..6543e584d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -1,16 +1,17 @@ pragma ComponentBehavior: Bound -import "./kblayout" -import QtQuick -import Quickshell -import Quickshell.Services.SystemTray import qs.components import qs.config +import Quickshell +import Quickshell.Services.SystemTray +import QtQuick + +import "./kblayout" Item { id: root - required property PopoutState popouts + required property Item wrapper readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null @@ -28,16 +29,15 @@ Item { Popout { name: "activewindow" sourceComponent: ActiveWindow { - popouts: root.popouts + wrapper: root.wrapper } } Popout { id: networkPopout - name: "network" sourceComponent: Network { - popouts: root.popouts + wrapper: root.wrapper view: "wireless" } } @@ -45,64 +45,60 @@ Item { Popout { name: "ethernet" sourceComponent: Network { - popouts: root.popouts + wrapper: root.wrapper view: "ethernet" } } Popout { id: passwordPopout - name: "wirelesspassword" sourceComponent: WirelessPassword { id: passwordComponent - - popouts: root.popouts - network: (networkPopout.item as Network)?.passwordNetwork ?? null + wrapper: root.wrapper + network: networkPopout.item?.passwordNetwork ?? null } Connections { + target: root.wrapper function onCurrentNameChanged() { // Update network immediately when password popout becomes active - if (root.popouts.currentName === "wirelesspassword") { + if (root.wrapper.currentName === "wirelesspassword") { // Set network immediately if available - if ((networkPopout.item as Network)?.passwordNetwork) { + if (networkPopout.item && networkPopout.item.passwordNetwork) { if (passwordPopout.item) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + passwordPopout.item.network = networkPopout.item.passwordNetwork; } } // Also try after a short delay in case networkPopout.item wasn't ready Qt.callLater(() => { - if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; } }, 100); } } - - target: root.popouts } Connections { + target: networkPopout function onItemChanged() { // When network popout loads, update password popout if it's active - if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { + if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { Qt.callLater(() => { - if ((networkPopout.item as Network)?.passwordNetwork) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + if (networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; } }); } } - - target: networkPopout } } Popout { name: "bluetooth" sourceComponent: Bluetooth { - popouts: root.popouts + wrapper: root.wrapper } } @@ -114,13 +110,15 @@ Item { Popout { name: "audio" sourceComponent: Audio { - popouts: root.popouts + wrapper: root.wrapper } } Popout { name: "kblayout" - sourceComponent: KbLayout {} + sourceComponent: KbLayout { + wrapper: root.wrapper + } } Popout { @@ -143,22 +141,22 @@ Item { sourceComponent: trayMenuComp Connections { + target: root.wrapper + function onHasCurrentChanged(): void { - if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { + if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } - - target: root.popouts } Component { id: trayMenuComp TrayMenu { - popouts: root.popouts - trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type + popouts: root.wrapper + trayItem: trayMenu.modelData.menu } } } @@ -169,7 +167,7 @@ Item { id: popout required property string name - readonly property bool shouldBeActive: root.popouts.currentName === name + readonly property bool shouldBeActive: root.wrapper.currentName === name anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 9b61e0372..7d74530e3 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,7 +1,7 @@ -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick.Layouts ColumnLayout { spacing: Appearance.spacing.small diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 53350ee73..5b32e4a6e 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" @@ -45,7 +45,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property + text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } @@ -123,6 +123,9 @@ ColumnLayout { } StateLayer { + color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: networkItem.loading || !Nmcli.wifiEnabled + function onClicked(): void { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); @@ -132,16 +135,13 @@ ColumnLayout { // Password is required - show password dialog root.passwordNetwork = network; root.showPasswordDialog = true; - root.popouts.currentName = "wirelesspassword"; + root.wrapper.currentName = "wirelesspassword"; }); // Clear connecting state if connection succeeds immediately (saved profile) // This is handled by the onActiveChanged connection below } } - - color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Nmcli.wifiEnabled } MaterialIcon { @@ -173,12 +173,12 @@ ColumnLayout { color: Colours.palette.m3primaryContainer StateLayer { + color: Colours.palette.m3onPrimaryContainer + disabled: Nmcli.scanning || !Nmcli.wifiEnabled + function onClicked(): void { Nmcli.rescanWifi(); } - - color: Colours.palette.m3onPrimaryContainer - disabled: Nmcli.scanning || !Nmcli.wifiEnabled } RowLayout { @@ -303,6 +303,9 @@ ColumnLayout { } StateLayer { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + function onClicked(): void { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); @@ -310,9 +313,6 @@ ColumnLayout { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } - - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading } MaterialIcon { @@ -334,6 +334,8 @@ ColumnLayout { } Connections { + target: Nmcli + function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; @@ -341,8 +343,8 @@ ColumnLayout { if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; } } } @@ -352,20 +354,17 @@ ColumnLayout { if (!Nmcli.scanning) scanIcon.rotation = 0; } - - target: Nmcli } Connections { + target: root.wrapper function onCurrentNameChanged(): void { // Clear password network when leaving password dialog - if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { + if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { root.showPasswordDialog = false; root.passwordNetwork = null; } } - - target: root.popouts } component Toggle: RowLayout { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 4347e39e1..9b743db19 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Widgets import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls StackView { id: root - required property PopoutState popouts + required property Item popouts required property QsMenuHandle trayItem - implicitWidth: currentItem?.implicitWidth ?? 0 - implicitHeight: currentItem?.implicitHeight ?? 0 + implicitWidth: currentItem.implicitWidth + implicitHeight: currentItem.implicitHeight initialItem: SubMenu { handle: root.trayItem @@ -26,12 +26,6 @@ StackView { popEnter: NoAnim {} popExit: NoAnim {} - Component { - id: subMenuComp - - SubMenu {} - } - component NoAnim: Transition { NumberAnimation { duration: 0 @@ -87,7 +81,6 @@ StackView { Loader { id: children - asynchronous: true anchors.left: parent.left anchors.right: parent.right @@ -97,6 +90,13 @@ StackView { implicitHeight: label.implicitHeight StateLayer { + anchors.margins: -Appearance.padding.small / 2 + anchors.leftMargin: -Appearance.padding.smaller + anchors.rightMargin: -Appearance.padding.smaller + + radius: item.radius + disabled: !item.modelData.enabled + function onClicked(): void { const entry = item.modelData; if (entry.hasChildren) @@ -109,25 +109,16 @@ StackView { root.popouts.hasCurrent = false; } } - - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller - - radius: item.radius - disabled: !item.modelData.enabled } Loader { id: icon - asynchronous: true anchors.left: parent.left active: item.modelData.icon !== "" sourceComponent: IconImage { - asynchronous: true implicitSize: label.implicitHeight source: item.modelData.icon @@ -158,7 +149,6 @@ StackView { Loader { id: expand - asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -175,7 +165,6 @@ StackView { } Loader { - asynchronous: true active: menu.isSubMenu sourceComponent: Item { @@ -197,12 +186,12 @@ StackView { color: Colours.palette.m3secondaryContainer StateLayer { + radius: parent.radius + color: Colours.palette.m3onSecondaryContainer + function onClicked(): void { root.pop(); } - - radius: parent.radius - color: Colours.palette.m3onSecondaryContainer } } @@ -227,4 +216,10 @@ StackView { } } } + + Component { + id: subMenuComp + + SubMenu {} + } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 8858273d7..96639e711 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -1,77 +1,59 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper property var network: null property bool isClosing: false - readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" - - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); } } } - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - - // Return to network popout - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); } } spacing: Appearance.spacing.normal + implicitWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled @@ -92,49 +74,16 @@ ColumnLayout { Keys.onEscapePressed: closeDialog() - Connections { - function onCurrentNameChanged() { - if (root.popouts.currentName === "wirelesspassword") { - // Update network when popout becomes active - Qt.callLater(() => { - // Try to get network from parent Content's networkPopout - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Force focus to password container when popout becomes active - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - }); - } - } - - target: root.popouts - } - - Timer { - id: focusTimer - - interval: 150 - onTriggered: { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - } - } - StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 - Keys.onEscapePressed: root.closeDialog() Behavior on opacity { Anim {} @@ -164,6 +113,8 @@ ColumnLayout { } } + Keys.onEscapePressed: root.closeDialog() + ColumnLayout { id: content @@ -189,7 +140,6 @@ ColumnLayout { StyledText { id: networkNameText - Layout.alignment: Qt.AlignHCenter text: { if (root.network) { @@ -205,11 +155,10 @@ ColumnLayout { } Timer { - property int attempts: 0 - interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) repeat: true + property int attempts: 0 onTriggered: { attempts++; // Keep trying to get network from Network component @@ -257,22 +206,15 @@ ColumnLayout { FocusScope { id: passwordContainer - - property string passwordBuffer: "" - objectName: "passwordContainer" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + focus: true activeFocusOnTab: true - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } + property string passwordBuffer: "" Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input @@ -280,11 +222,6 @@ ColumnLayout { forceActiveFocus(); } - if (event.key === Qt.Key_Escape) { - event.accepted = false; - closeDialog(); - } - // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; @@ -303,16 +240,13 @@ ColumnLayout { } event.accepted = true; } else if (event.text && event.text.length > 0) { - if (event.key === Qt.Key_Tab) { - event.accepted = false; - return; - } passwordBuffer += event.text; event.accepted = true; } } Connections { + target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly @@ -321,19 +255,23 @@ ColumnLayout { connectButton.hasError = false; } } - - target: root } Timer { id: passwordFocusTimer - interval: 50 onTriggered: { passwordContainer.forceActiveFocus(); } } + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } + StyledRect { anchors.fill: parent radius: Appearance.rounding.normal @@ -363,13 +301,13 @@ ColumnLayout { } StateLayer { - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } - hoverEnabled: false cursorShape: Qt.IBeamCursor radius: Appearance.rounding.normal + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } } StyledText { @@ -553,14 +491,45 @@ ColumnLayout { } } - Timer { - id: connectionMonitor + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } - property int repeatCount: 0 + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + Timer { + id: connectionMonitor interval: 1000 repeat: true triggeredOnStart: false + property int repeatCount: 0 onTriggered: { repeatCount++; @@ -576,7 +545,6 @@ ColumnLayout { Timer { id: connectionSuccessTimer - interval: 500 onTriggered: { // Double-check connection is still active @@ -587,8 +555,8 @@ ColumnLayout { connectButton.connecting = false; connectButton.text = qsTr("Connect"); // Return to network popout on successful connection - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; } closeDialog(); } @@ -597,12 +565,12 @@ ColumnLayout { } Connections { + target: Nmcli function onActiveChanged() { if (root.shouldBeVisible) { root.checkConnectionStatus(); } } - function onConnectionFailed(ssid: string) { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); @@ -615,7 +583,23 @@ ColumnLayout { Nmcli.forgetNetwork(ssid); } } + } - target: Nmcli + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } } } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 06bec9b0b..05a1d3c9e 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.services import qs.config -import qs.modules.controlcenter import qs.modules.windowinfo +import qs.modules.controlcenter +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick Item { id: root @@ -17,12 +17,11 @@ Item { readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight - readonly property Item current: (content.item as Content)?.current ?? null + readonly property Item current: content.item?.current ?? null - property alias currentName: popoutState.currentName + property string currentName property real currentCenter - property alias hasCurrent: popoutState.hasCurrent - readonly property PopoutState state: popoutState + property bool hasCurrent property string detachedMode property string queuedMode @@ -60,7 +59,7 @@ Item { Keys.onEscapePressed: { // Forward escape to password popout if active, otherwise close if (currentName === "wirelesspassword" && content.item) { - const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); + const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); if (passwordPopout && passwordPopout.item) { passwordPopout.item.closeDialog(); return; @@ -76,12 +75,6 @@ Item { } } - PopoutState { - id: popoutState - - onDetachRequested: mode => root.detach(mode) - } - HyprlandFocusGrab { active: root.isDetached windows: [QsWindow.window] @@ -112,7 +105,7 @@ Item { anchors.verticalCenter: parent.verticalCenter sourceComponent: Content { - popouts: popoutState + wrapper: root } } @@ -131,12 +124,12 @@ Item { anchors.centerIn: parent sourceComponent: ControlCenter { + screen: root.screen + active: root.queuedMode + function close(): void { root.close(); } - - screen: root.screen - active: root.queuedMode } } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 3e5819027..94b6f7ec5 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -4,25 +4,30 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.components +import qs.components.controls import qs.services import qs.config +import qs.utils + +import "." ColumnLayout { id: root - function refresh() { - kb.refresh(); - } + required property Item wrapper spacing: Appearance.spacing.small width: Config.bar.sizes.kbLayoutWidth - Component.onCompleted: kb.start() - KbLayoutModel { id: kb } + function refresh() { + kb.refresh(); + } + Component.onCompleted: kb.start() + StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small @@ -32,7 +37,6 @@ ColumnLayout { ListView { id: list - model: kb.visibleModel Layout.fillWidth: true @@ -81,45 +85,44 @@ ColumnLayout { } delegate: Item { - id: kbDelegate - required property int layoutIndex required property string label - readonly property bool isDisabled: layoutIndex > 3 width: list.width height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) - ToolTip.visible: isDisabled && layer.containsMouse - ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + + readonly property bool isDisabled: layoutIndex > 3 StateLayer { id: layer - - function onClicked(): void { - if (!kbDelegate.isDisabled) - kb.switchTo(kbDelegate.layoutIndex); - } - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 + radius: Appearance.rounding.full - enabled: !kbDelegate.isDisabled + enabled: !isDisabled + + function onClicked(): void { + if (!isDisabled) + kb.switchTo(layoutIndex); + } } StyledText { id: rowText - anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right anchors.leftMargin: Appearance.padding.small anchors.rightMargin: Appearance.padding.small - text: kbDelegate.label + text: label elide: Text.ElideRight - opacity: kbDelegate.isDisabled ? 0.4 : 1.0 + opacity: isDisabled ? 0.4 : 1.0 } + + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" } } @@ -129,7 +132,7 @@ ColumnLayout { Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small - implicitHeight: 1 + height: 1 color: Colours.palette.m3onSurfaceVariant opacity: 0.35 } @@ -160,18 +163,16 @@ ColumnLayout { } Connections { + target: kb function onActiveLabelChanged() { if (!activeRow.visible) return; popIn.restart(); } - - target: kb } SequentialAnimation { id: popIn - running: false ParallelAnimation { diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 4caebbd29..437109530 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -1,20 +1,24 @@ pragma ComponentBehavior: Bound import QtQuick + +import Quickshell import Quickshell.Io -import Caelestia -import qs.config -// TODO: handle this better later +import qs.config +import Caelestia Item { id: model + visible: false + ListModel { + id: _visibleModel + } property alias visibleModel: _visibleModel + property string activeLabel: "" property int activeIndex: -1 - property var _xkbMap: ({}) - property bool _notifiedLimit: false function start() { _xkbXmlBase.running = true; @@ -31,6 +35,31 @@ Item { _switchProc.running = true; } + ListModel { + id: _layoutsModel + } + + property var _xkbMap: ({}) + property bool _notifiedLimit: false + + Process { + id: _xkbXmlBase + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) + _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + } + function _buildXmlMap(xml) { const map = {}; @@ -76,84 +105,8 @@ Item { return `${lang} (${code})`; } - function _setLayouts(raw) { - const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); - - const seen = new Set(); - let idx = 0; - - for (const p of parts) { - if (seen.has(p)) - continue; - seen.add(p); - _layoutsModel.append({ - layoutIndex: idx, - token: p, - label: _pretty(p) - }); - idx++; - } - } - - function _rebuildVisible() { - _visibleModel.clear(); - - let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); - - arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); - - if (!Config.utilities.toasts.kbLimit) - return; - - if (_layoutsModel.count > 4) { - Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); - } - } - - function _pretty(token) { - const code = token.replace(/\(.*\)$/, "").trim(); - if (_xkbMap[code]) - return code.toUpperCase() + " - " + _xkbMap[code]; - return code.toUpperCase() + " - " + code; - } - - visible: false - - ListModel { - id: _visibleModel - } - - ListModel { - id: _layoutsModel - } - - Process { - id: _xkbXmlBase - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] - stdout: StdioCollector { - onStreamFinished: model._buildXmlMap(text) - } - onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property - _xkbXmlEvdev.running = true - } - - Process { - id: _xkbXmlEvdev - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] - stdout: StdioCollector { - onStreamFinished: model._buildXmlMap(text) - } - } - Process { id: _getKbLayoutOpt - command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { onStreamFinished: { @@ -161,7 +114,7 @@ Item { const j = JSON.parse(text); const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { - model._setLayouts(raw); + _setLayouts(raw); _fetchActiveLayouts.running = true; return; } @@ -173,7 +126,6 @@ Item { Process { id: _fetchLayoutsFromDevices - command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -182,7 +134,7 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const raw = (kb?.layout || "").trim(); if (raw.length) - model._setLayouts(raw); + _setLayouts(raw); } catch (e) {} _fetchActiveLayouts.running = true; } @@ -191,7 +143,6 @@ Item { Process { id: _fetchActiveLayouts - command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -200,22 +151,66 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const idx = kb?.active_layout_index ?? -1; - model.activeIndex = idx >= 0 ? idx : -1; - model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + activeIndex = idx >= 0 ? idx : -1; + activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; } catch (e) { - model.activeIndex = -1; - model.activeLabel = ""; + activeIndex = -1; + activeLabel = ""; } - model._rebuildVisible(); + _rebuildVisible(); } } } Process { id: _switchProc - onRunningChanged: if (!running) _fetchActiveLayouts.running = true } + + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) + continue; + seen.add(p); + _layoutsModel.append({ + layoutIndex: idx, + token: p, + label: _pretty(p) + }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!Config.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) + return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index f542b5970..4aacfad99 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -18,7 +18,6 @@ Item { property alias active: session.active property alias navExpanded: session.navExpanded - readonly property bool initialOpeningComplete: panes.initialOpeningComplete readonly property Session session: Session { id: session @@ -43,7 +42,6 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 - asynchronous: true active: root.floating visible: active @@ -62,6 +60,8 @@ Item { color: Colours.tPalette.m3surfaceContainer CustomMouseArea { + anchors.fill: parent + function onWheel(event: WheelEvent): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!panes.initialOpeningComplete) { @@ -73,8 +73,6 @@ Item { else if (event.angleDelta.y > 0) root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); } - - anchors.fill: parent } NavRail { @@ -97,4 +95,6 @@ Item { session: root.session } } + + readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 037ea0d64..e61a741a3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config import qs.modules.controlcenter +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -43,7 +43,6 @@ Item { Loader { Layout.topMargin: Appearance.spacing.large - asynchronous: true active: !root.session.floating visible: active @@ -59,6 +58,8 @@ Item { StateLayer { id: normalWinState + color: Colours.palette.m3onPrimaryContainer + function onClicked(): void { root.session.root.close(); WindowFactory.create(null, { @@ -66,8 +67,6 @@ Item { navExpanded: root.session.navExpanded }); } - - color: Colours.palette.m3onPrimaryContainer } MaterialIcon { @@ -122,7 +121,6 @@ Item { NavItem { required property int index - Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label @@ -176,6 +174,8 @@ Item { implicitHeight: icon.implicitHeight + Appearance.padding.small StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked(): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { @@ -183,8 +183,6 @@ Item { } root.session.active = item.label; } - - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5660ea7d3..ab2f808e9 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -7,13 +7,13 @@ import "appearance" import "taskbar" import "launcher" import "dashboard" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.services import qs.config import qs.modules.controlcenter +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ClippingRectangle { id: root @@ -37,26 +37,25 @@ ClippingRectangle { } Connections { + target: root.session + function onActiveIndexChanged(): void { root.focus = true; } - - target: root.session } ColumnLayout { id: layout - property bool animationComplete: true - property bool initialOpeningComplete: false - spacing: 0 y: -root.session.activeIndex * root.height clip: true + property bool animationComplete: true + property bool initialOpeningComplete: false + Timer { id: animationDelayTimer - interval: Appearance.anim.durations.normal onTriggered: { layout.animationComplete = true; @@ -65,7 +64,6 @@ ClippingRectangle { Timer { id: initialOpeningTimer - interval: Appearance.anim.durations.large running: true onTriggered: { @@ -78,7 +76,6 @@ ClippingRectangle { Pane { required property int index - paneIndex: index componentPath: PaneRegistry.getByIndex(index).component } @@ -89,12 +86,11 @@ ClippingRectangle { } Connections { + target: root.session function onActiveIndexChanged(): void { layout.animationComplete = false; animationDelayTimer.restart(); } - - target: root.session } } @@ -103,6 +99,10 @@ ClippingRectangle { required property int paneIndex required property string componentPath + + implicitWidth: root.width + implicitHeight: root.height + property bool hasBeenLoaded: false function updateActive(): void { @@ -125,14 +125,10 @@ ClippingRectangle { loader.active = shouldBeActive; } - implicitWidth: root.width - implicitHeight: root.height - Loader { id: loader anchors.fill: parent - asynchronous: true clip: false active: false @@ -160,22 +156,20 @@ ClippingRectangle { } Connections { + target: root.session function onActiveIndexChanged(): void { pane.updateActive(); } - - target: root.session } Connections { + target: layout function onInitialOpeningCompleteChanged(): void { pane.updateActive(); } function onAnimationCompleteChanged(): void { pane.updateActive(); } - - target: layout } } } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index e38396810..8a8545f0f 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,5 +1,5 @@ -import "./state" import QtQuick +import "./state" import qs.modules.controlcenter QtObject { diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 266af9095..abcf5df19 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick -import Quickshell import qs.components import qs.services +import Quickshell +import QtQuick Singleton { id: root @@ -45,13 +45,13 @@ Singleton { ControlCenter { id: cc - function close(): void { - win.destroy(); - } - anchors.fill: parent screen: win.screen floating: true + + function close(): void { + win.destroy(); + } } Behavior on color { diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index a55445c5a..fb7160893 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -1,8 +1,8 @@ -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick StyledRect { id: root @@ -34,11 +34,11 @@ StyledRect { implicitHeight: closeIcon.implicitHeight + Appearance.padding.small StateLayer { + radius: Appearance.rounding.full + function onClicked(): void { QsWindow.window.destroy(); } - - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c42220762..f29f7ab3d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -4,19 +4,19 @@ import ".." import "../components" import "./sections" import "../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Models import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.components.images import qs.services import qs.config import qs.utils +import Caelestia.Models +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -54,6 +54,8 @@ Item { property real visualiserRounding: Config.background.visualiser.rounding ?? 1 property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 + anchors.fill: parent + function saveConfig() { Config.appearance.anim.durations.scale = root.animDurationsScale; @@ -95,8 +97,6 @@ Item { Config.save(); } - anchors.fill: parent - Component { id: appearanceRightContentComponent @@ -124,7 +124,6 @@ Item { Layout.fillHeight: true Layout.bottomMargin: -Appearance.padding.large * 2 - asynchronous: true active: { const isActive = root.session.activeIndex === 3; const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; @@ -152,11 +151,10 @@ Item { anchors.fill: parent leftContent: Component { + StyledFlickable { id: sidebarFlickable - readonly property var rootPane: root - flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -166,14 +164,14 @@ Item { ColumnLayout { id: sidebarLayout - - readonly property var rootPane: sidebarFlickable.rootPane - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded - anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small + readonly property var rootPane: sidebarFlickable.rootPane + + readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded + RowLayout { spacing: Appearance.spacing.smaller @@ -220,37 +218,31 @@ Item { AnimationsSection { id: animationsSection - rootPane: sidebarFlickable.rootPane } FontsSection { id: fontsSection - rootPane: sidebarFlickable.rootPane } ScalesSection { id: scalesSection - rootPane: sidebarFlickable.rootPane } TransparencySection { id: transparencySection - rootPane: sidebarFlickable.rootPane } BorderSection { id: borderSection - rootPane: sidebarFlickable.rootPane } BackgroundSection { id: backgroundSection - rootPane: sidebarFlickable.rootPane } } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index e4d8a0333..0cba5cecd 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 8b50c1242..9d6bc6ebc 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -55,6 +55,9 @@ CollapsibleSection { SectionContainer { id: posContainer + contentSpacing: Appearance.spacing.small + z: 1 + readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] @@ -64,9 +67,6 @@ CollapsibleSection { rootPane.saveConfig(); } - contentSpacing: Appearance.spacing.small - z: 1 - StyledText { text: qsTr("Positioning") font.pointSize: Appearance.font.size.larger @@ -79,22 +79,19 @@ CollapsibleSection { menuItems: [ MenuItem { - property string val: "top" - text: qsTr("Top") icon: "vertical_align_top" + property string val: "top" }, MenuItem { - property string val: "middle" - text: qsTr("Middle") icon: "vertical_align_center" + property string val: "middle" }, MenuItem { - property string val: "bottom" - text: qsTr("Bottom") icon: "vertical_align_bottom" + property string val: "bottom" } ] @@ -116,22 +113,19 @@ CollapsibleSection { menuItems: [ MenuItem { - property string val: "left" - text: qsTr("Left") icon: "align_horizontal_left" + property string val: "left" }, MenuItem { - property string val: "center" - text: qsTr("Center") icon: "align_horizontal_center" + property string val: "center" }, MenuItem { - property string val: "right" - text: qsTr("Right") icon: "align_horizontal_right" + property string val: "right" } ] diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index e0c677cf7..9532d70d6 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -50,7 +50,7 @@ CollapsibleSection { label: qsTr("Border thickness") value: rootPane.borderThickness - from: 0 + from: 0.1 to: 100 decimals: 1 suffix: "px" diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index b954cd458..95cb4b725 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts CollapsibleSection { title: qsTr("Color scheme") @@ -35,7 +35,6 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -54,7 +53,6 @@ CollapsibleSection { Timer { id: reloadTimer - interval: 300 onTriggered: { Schemes.reload(); @@ -84,7 +82,6 @@ CollapsibleSection { MaterialIcon { id: iconPlaceholder - visible: false text: "circle" font.pointSize: Appearance.font.size.large @@ -131,7 +128,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -141,6 +137,8 @@ CollapsibleSection { } } } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index b3cc4cfba..3aa17dd9c 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts CollapsibleSection { title: qsTr("Color variant") @@ -32,7 +32,6 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -49,7 +48,6 @@ CollapsibleSection { Timer { id: reloadTimer - interval: 300 onTriggered: { Schemes.reload(); @@ -85,6 +83,8 @@ CollapsibleSection { font.pointSize: Appearance.font.size.large } } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 47b738f19..3988863af 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -19,53 +19,51 @@ CollapsibleSection { showBackground: true CollapsibleSection { - id: sansFontSection - - title: qsTr("Sans-serif font family") + id: materialFontSection + title: qsTr("Material font family") expanded: true showBackground: true nested: true Loader { + id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded + active: materialFontSection.expanded sourceComponent: StyledListView { - id: sansFontList - - property alias contentHeight: sansFontList.contentHeight + id: materialFontList + property alias contentHeight: materialFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList + flickable: materialFontList } delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilySans width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { - rootPane.fontFamilySans = modelData; + rootPane.fontFamilyMaterial = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilySansRow + id: fontFamilyMaterialRow anchors.left: parent.left anchors.right: parent.right @@ -84,7 +82,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -94,6 +91,8 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -101,7 +100,6 @@ CollapsibleSection { CollapsibleSection { id: monoFontSection - title: qsTr("Monospace font family") expanded: false showBackground: true @@ -110,12 +108,10 @@ CollapsibleSection { Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true active: monoFontSection.expanded sourceComponent: StyledListView { id: monoFontList - property alias contentHeight: monoFontList.contentHeight clip: true @@ -129,14 +125,14 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -165,7 +161,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -175,61 +170,58 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { - id: materialFontSection - - title: qsTr("Material font family") + id: sansFontSection + title: qsTr("Sans-serif font family") expanded: false showBackground: true nested: true Loader { - id: materialFontLoader - Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded + active: sansFontSection.expanded sourceComponent: StyledListView { - id: materialFontList - - property alias contentHeight: materialFontList.contentHeight + id: sansFontList + property alias contentHeight: sansFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) + model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList + flickable: sansFontList } delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; + rootPane.fontFamilySans = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilyMaterialRow + id: fontFamilySansRow anchors.left: parent.left anchors.right: parent.right @@ -248,7 +240,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -258,6 +249,8 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index 6d5d5b303..b0e6e38b8 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index c63c73aaf..04eed9113 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 77582f9c6..9a48629c1 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 172132f6b..01d90be70 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -23,9 +23,9 @@ Item { anchors.fill: parent leftContent: Component { + StyledFlickable { id: leftAudioFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height @@ -94,7 +94,6 @@ Item { color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -127,6 +126,8 @@ Item { font.weight: Audio.sink?.id === modelData.id ? 500 : 400 } } + + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -171,7 +172,6 @@ Item { color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -204,6 +204,8 @@ Item { font.weight: Audio.source?.id === modelData.id ? 500 : 400 } } + + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -215,7 +217,6 @@ Item { rightContent: Component { StyledFlickable { id: rightAudioFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.height @@ -264,7 +265,6 @@ Item { StyledInputField { id: outputVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -276,6 +276,15 @@ Item { text = Math.round(Audio.volume * 100).toString(); } + Connections { + target: Audio + function onVolumeChanged() { + if (!outputVolumeInput.hasFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -291,16 +300,6 @@ Item { text = Math.round(Audio.volume * 100).toString(); } } - - Connections { - function onVolumeChanged() { - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } - } - - target: Audio - } } StyledText { @@ -337,7 +336,6 @@ Item { StyledSlider { id: outputVolumeSlider - Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -382,7 +380,6 @@ Item { StyledInputField { id: inputVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -394,6 +391,15 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } + Connections { + target: Audio + function onSourceVolumeChanged() { + if (!inputVolumeInput.hasFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -409,16 +415,6 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } } - - Connections { - function onSourceVolumeChanged() { - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - - target: Audio - } } StyledText { @@ -455,7 +451,6 @@ Item { StyledSlider { id: inputVolumeSlider - Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -516,7 +511,6 @@ Item { StyledInputField { id: streamVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -528,6 +522,15 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } + Connections { + target: modelData + function onAudioChanged() { + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -543,16 +546,6 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } } - - Connections { - function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); - } - } - - target: modelData - } } StyledText { @@ -600,13 +593,12 @@ Item { } Connections { + target: modelData function onAudioChanged() { if (modelData?.audio) { value = modelData.audio.volume; } } - - target: modelData } } } diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 97ea99c57..7d3b9ca33 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import Quickshell.Bluetooth -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.config +import Quickshell.Widgets +import Quickshell.Bluetooth +import QtQuick SplitPaneWithDetails { id: root @@ -53,7 +53,6 @@ SplitPaneWithDetails { rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 9b347acdc..bc276e097 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts StyledFlickable { id: root @@ -241,13 +241,13 @@ StyledFlickable { scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName + function onClicked(): void { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName } MaterialIcon { @@ -279,6 +279,8 @@ StyledFlickable { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) @@ -286,8 +288,6 @@ StyledFlickable { else deviceNameEdit.accepted(); } - - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -359,7 +359,6 @@ StyledFlickable { RowLayout { id: batteryPercent - Layout.topMargin: Appearance.spacing.small / 2 Layout.fillWidth: true Layout.preferredHeight: Appearance.padding.smaller @@ -630,11 +629,11 @@ StyledFlickable { StateLayer { id: fabState + color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer + function onClicked(): void { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } - - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index b53829b93..2a2bde934 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts DeviceList { id: root @@ -222,6 +222,9 @@ DeviceList { } StateLayer { + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + disabled: device.loading + function onClicked(): void { if (device.loading) return; @@ -236,9 +239,6 @@ DeviceList { } } } - - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - disabled: device.loading } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 6936c1ea2..c5472406b 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -131,11 +131,11 @@ ColumnLayout { implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 StateLayer { + radius: Appearance.rounding.small + function onClicked(): void { adapterPickerButton.expanded = !adapterPickerButton.expanded; } - - radius: Appearance.rounding.small } RowLayout { @@ -210,12 +210,12 @@ ColumnLayout { implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 StateLayer { + disabled: !adapterPickerButton.expanded + function onClicked(): void { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } - - disabled: !adapterPickerButton.expanded } RowLayout { @@ -381,13 +381,13 @@ ColumnLayout { scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingAdapterName + function onClicked(): void { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingAdapterName } MaterialIcon { @@ -419,6 +419,8 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) @@ -426,8 +428,6 @@ ColumnLayout { else adapterNameEdit.accepted(); } - - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 15b1896cb..ab707fb73 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,11 +1,11 @@ import ".." -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -40,7 +40,6 @@ StyledRect { GridLayout { id: buttonGrid - Layout.alignment: Qt.AlignHCenter rowSpacing: Appearance.spacing.small columnSpacing: Appearance.spacing.small @@ -49,19 +48,18 @@ StyledRect { Repeater { id: repeater - model: root.options delegate: TextButton { id: button - required property int index required property var modelData - property bool _checked: false - Layout.fillWidth: true text: modelData.label + + property bool _checked: false + checked: _checked toggle: false type: TextButton.Tonal @@ -70,7 +68,8 @@ StyledRect { Component.onCompleted: { if (modelData.state !== undefined && modelData.state) { _checked = modelData.state; - } else if (root.rootItem && modelData.propertyName) { + } + else if (root.rootItem && modelData.propertyName) { const propName = modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index c150abd7d..a5d06471c 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -36,7 +36,6 @@ Item { id: headerLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null } @@ -45,7 +44,6 @@ Item { id: topContentLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.topContent visible: root.topContent !== null } @@ -57,7 +55,6 @@ Item { required property Component modelData Layout.fillWidth: true - asynchronous: true sourceComponent: modelData } } @@ -66,7 +63,6 @@ Item { id: bottomContentLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.bottomContent visible: root.bottomContent !== null } diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 2134d8cfe..722f9a16e 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -23,7 +23,6 @@ ColumnLayout { property Component headerComponent: null property Component titleSuffix: null property bool showHeader: true - property alias view: view signal itemSelected(var item) @@ -33,7 +32,6 @@ ColumnLayout { id: headerLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null && root.showHeader } @@ -52,7 +50,6 @@ ColumnLayout { } Loader { - asynchronous: true sourceComponent: root.titleSuffix visible: root.titleSuffix !== null } @@ -62,6 +59,8 @@ ColumnLayout { } } + property alias view: view + StyledText { visible: root.description !== "" Layout.fillWidth: true diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index ac9c3371a..5d80dbec2 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.config +import QtQuick SequentialAnimation { id: root diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 105270451..169d63653 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -1,11 +1,11 @@ import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 6d392f5ea..0dc190c05 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index df3acf41e..11b3f70dd 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -21,9 +21,6 @@ ColumnLayout { property int decimals: 1 // Number of decimal places to show (default: 1) property var formatValueFunction: null // Optional custom format function property var parseValueFunction: null // Optional custom parse function - property bool _initialized: false - - signal valueModified(real newValue) function formatValue(val: real): string { if (formatValueFunction) { @@ -52,6 +49,10 @@ ColumnLayout { return parseFloat(text); } + signal valueModified(real newValue) + + property bool _initialized: false + spacing: Appearance.spacing.small Component.onCompleted: { @@ -61,14 +62,6 @@ ColumnLayout { }); } - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.hasFocus) { - inputField.text = root.formatValue(root.value); - } - } - RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal @@ -85,7 +78,6 @@ ColumnLayout { StyledInputField { id: inputField - Layout.preferredWidth: 70 validator: root.validator @@ -152,6 +144,14 @@ ColumnLayout { to: root.to stepSize: root.stepSize + // Use Binding to allow slider to move freely during dragging + Binding { + target: slider + property: "value" + value: root.value + when: !slider.pressed + } + onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) @@ -168,13 +168,13 @@ ColumnLayout { inputField.text = root.formatValue(newValue); } } + } - // Use Binding to allow slider to move freely during dragging - Binding { - target: slider - property: "value" - value: root.value - when: !slider.pressed + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + // Only update if component is initialized to avoid issues during creation + if (root._initialized && !inputField.hasFocus) { + inputField.text = root.formatValue(root.value); } } } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 5bf5c41ad..89504a0b8 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,26 +1,28 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.components.effects import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts RowLayout { id: root + spacing: 0 + property Component leftContent: null property Component rightContent: null + property real leftWidthRatio: 0.4 property int leftMinimumWidth: 420 property var leftLoaderProperties: ({}) property var rightLoaderProperties: ({}) + property alias leftLoader: leftLoader property alias rightLoader: rightLoader - spacing: 0 - Item { id: leftPane @@ -47,7 +49,6 @@ RowLayout { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { @@ -89,7 +90,6 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 - asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index e8dcb269c..ce8c9d07d 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.effects +import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -48,17 +48,11 @@ Item { nextComponent = targetComponent; } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = root.paneIdGenerator(pane); - } - Loader { id: rightLoader anchors.fill: parent - asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -79,6 +73,11 @@ Item { ] } } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } } } } @@ -87,7 +86,6 @@ Item { id: overlayLoader anchors.fill: parent - asynchronous: true z: 1000 sourceComponent: root.overlayComponent active: root.overlayComponent !== null diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 6a65c8e15..ed6bb40a8 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import Caelestia.Models import qs.components import qs.components.controls import qs.components.effects import qs.components.images import qs.services import qs.config +import Caelestia.Models +import QtQuick GridView { id: root @@ -32,24 +32,25 @@ GridView { delegate: Item { required property var modelData required property int index - readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Appearance.spacing.normal / 2 - readonly property real itemRadius: Appearance.rounding.normal width: root.cellWidth height: root.cellHeight - StateLayer { - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } + readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent + readonly property real itemMargin: Appearance.spacing.normal / 2 + readonly property real itemRadius: Appearance.rounding.normal + StateLayer { anchors.fill: parent anchors.leftMargin: itemMargin anchors.rightMargin: itemMargin anchors.topMargin: itemMargin anchors.bottomMargin: itemMargin radius: itemRadius + + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } } StyledClippingRect { @@ -116,7 +117,6 @@ GridView { id: fallbackTimer property bool triggered: false - interval: 800 running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true @@ -154,16 +154,16 @@ GridView { opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } } } @@ -201,7 +201,6 @@ GridView { StyledText { id: filenameText - anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom @@ -219,16 +218,16 @@ GridView { opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } } } } diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index bd6b9d5f1..df29f0964 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -40,6 +40,8 @@ Item { property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true + anchors.fill: parent + function saveConfig() { Config.dashboard.enabled = root.enabled; Config.dashboard.showOnHover = root.showOnHover; @@ -60,11 +62,8 @@ Item { Config.save(); } - anchors.fill: parent - ClippingRectangle { id: dashboardClippingRect - anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -81,14 +80,12 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large - asynchronous: true sourceComponent: dashboardContentComponent } } InnerBorder { id: dashboardBorder - leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -98,7 +95,6 @@ Item { StyledFlickable { id: dashboardFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: dashboardLayout.height @@ -108,7 +104,6 @@ Item { ColumnLayout { id: dashboardLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 61e83d3c7..95e7531ed 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -1,11 +1,11 @@ import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts SectionContainer { id: root diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index eebf5fd29..ac84752b6 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -5,8 +5,8 @@ import QtQuick.Layouts import Quickshell.Services.UPower import qs.components import qs.components.controls -import qs.services import qs.config +import qs.services SectionContainer { id: root diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 677cf4182..1df96403f 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -2,20 +2,19 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import "../../launcher/services" -import "../../../utils/scripts/fuzzysort.js" as Fuzzy -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import "../../../utils/scripts/fuzzysort.js" as Fuzzy Item { id: root @@ -25,8 +24,21 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false property bool favouriteChecked: false - property string searchText: "" - property list filteredApps: [] + + anchors.fill: parent + + onSelectedAppChanged: { + root.session.launcher.active = root.selectedApp; + updateToggleState(); + } + + Connections { + target: root.session.launcher + function onActiveChanged() { + root.selectedApp = root.session.launcher.active; + updateToggleState(); + } + } function updateToggleState() { if (!root.selectedApp) { @@ -65,22 +77,147 @@ Item { Config.save(); } + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + favouriteApps: Config.launcher.favouriteApps + entries: DesktopEntries.applications.values + } + + property string searchText: "" + + // Helper function to get categories for an app (returns array) + function getAppCategories(appId: string): list { + const cats = []; + if (!Config.launcher.categories) + return cats; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || !category.apps) + continue; + + // Check if this app is in this category's apps list + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + cats.push(category.name); + break; + } + } + } + } + return cats; + } + + // Helper function to check if app has a specific category + function appHasCategory(appId: string, categoryName: string): bool { + if (!Config.launcher.categories) + return false; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || category.name.toLowerCase() !== categoryName.toLowerCase()) + continue; + if (!category.apps) + continue; + + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + return true; + } + } + } + } + return false; + } + + // Helper function to toggle category for an app + function toggleAppCategory(appId: string, categoryName: string): void { + if (!Config.launcher.categories) + return; + + const newCategories = []; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category) + continue; + + const newCategory = { + name: category.name, + icon: category.icon, + apps: [] + }; + + // Copy existing apps + if (category.apps && typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + newCategory.apps.push(category.apps[j]); + } + } + + // Toggle this app in this category + if (category.name.toLowerCase() === categoryName.toLowerCase()) { + const index = newCategory.apps.indexOf(appId); + if (index >= 0) { + newCategory.apps.splice(index, 1); + } else { + newCategory.apps.push(appId); + } + } + + newCategories.push(newCategory); + } + + Config.launcher.categories = newCategories; + Config.save(); + } + function filterApps(search: string): list { - if (!search || search.trim() === "") { - const apps = []; + let baseApps = []; + + // Filter by category first + if (root.activeCategory === "all") { + for (let i = 0; i < allAppsDb.apps.length; i++) { + baseApps.push(allAppsDb.apps[i]); + } + } else if (root.activeCategory === "favourites") { + for (let i = 0; i < allAppsDb.apps.length; i++) { + const app = allAppsDb.apps[i]; + const appId = app.id || app.entry?.id; + if (Config.launcher.favouriteApps && Config.launcher.favouriteApps.includes(appId)) { + baseApps.push(app); + } + } + } else { + // Custom category + console.log(`Filtering for category: ${root.activeCategory}`); for (let i = 0; i < allAppsDb.apps.length; i++) { - apps.push(allAppsDb.apps[i]); + const app = allAppsDb.apps[i]; + const appId = app.id || app.entry?.id; + if (appHasCategory(appId, root.activeCategory)) { + console.log(`Found app in category: ${appId}`); + baseApps.push(app); + } } - return apps; + console.log(`Total apps in ${root.activeCategory}: ${baseApps.length}`); + } + + // Then filter by search text + if (!search || search.trim() === "") { + return baseApps; } - if (!allAppsDb.apps || allAppsDb.apps.length === 0) { + if (baseApps.length === 0) { return []; } const preparedApps = []; - for (let i = 0; i < allAppsDb.apps.length; i++) { - const app = allAppsDb.apps[i]; + for (let i = 0; i < baseApps.length; i++) { + const app = baseApps[i]; const name = app.name || app.entry?.name || ""; preparedApps.push({ _item: app, @@ -97,57 +234,40 @@ Item { return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); } + property list filteredApps: [] + function updateFilteredApps() { filteredApps = filterApps(searchText); } - anchors.fill: parent - - onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; - updateToggleState(); - } - onSearchTextChanged: { updateFilteredApps(); } - Component.onCompleted: { - updateFilteredApps(); - } - - Connections { - function onActiveChanged() { - root.selectedApp = root.session.launcher.active; - updateToggleState(); - } + property string activeCategory: "all" - target: root.session.launcher + onActiveCategoryChanged: { + updateFilteredApps(); } - AppDb { - id: allAppsDb - - path: `${Paths.state}/apps.sqlite` - favouriteApps: Config.launcher.favouriteApps - entries: DesktopEntries.applications.values + Component.onCompleted: { + updateFilteredApps(); } Connections { + target: allAppsDb function onAppsChanged() { updateFilteredApps(); } - - target: allAppsDb } SplitPaneLayout { anchors.fill: parent leftContent: Component { + ColumnLayout { id: leftLauncherLayout - anchors.fill: parent spacing: Appearance.spacing.small @@ -186,6 +306,86 @@ Item { } } + // Category tabs + StyledFlickable { + Layout.fillWidth: true + Layout.preferredHeight: categoryRow.height + Layout.topMargin: Appearance.spacing.normal + flickableDirection: Flickable.HorizontalFlick + contentWidth: categoryRow.width + clip: true + + Row { + id: categoryRow + spacing: Appearance.spacing.small + + Repeater { + model: [ + { + id: "all", + name: qsTr("All"), + icon: "apps" + }, + { + id: "favourites", + name: qsTr("Favourites"), + icon: "favorite" + } + ].concat(Config.launcher.categories.map(cat => ({ + id: cat.name.toLowerCase(), + name: cat.name, + icon: cat.icon + }))) + + delegate: StyledRect { + required property var modelData + + property bool isActive: root.activeCategory === modelData.id + + implicitWidth: tabContent.width + Appearance.padding.normal * 2 + implicitHeight: tabContent.height + Appearance.padding.smaller * 2 + + color: isActive ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + StateLayer { + radius: parent.radius + function onClicked(): void { + root.activeCategory = modelData.id; + } + } + + Row { + id: tabContent + anchors.centerIn: parent + spacing: Appearance.spacing.smaller + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.pointSize: Appearance.font.size.small + font.weight: isActive ? 500 : 400 + color: isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + } + } + } + } + StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) @@ -284,7 +484,6 @@ Item { Loader { id: appsListLoader - Layout.fillWidth: true Layout.fillHeight: true asynchronous: true @@ -307,20 +506,16 @@ Item { delegate: StyledRect { required property var modelData - readonly property bool isSelected: root.selectedApp === modelData - width: parent ? parent.width : 0 implicitHeight: 40 + readonly property bool isSelected: root.selectedApp === modelData + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 @@ -328,6 +523,10 @@ Item { } } + Component.onCompleted: { + opacity = 1; + } + StateLayer { function onClicked(): void { root.session.launcher.active = modelData; @@ -343,7 +542,6 @@ Item { spacing: Appearance.spacing.normal IconImage { - asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { @@ -359,11 +557,9 @@ Item { } Loader { + Layout.alignment: Qt.AlignVCenter readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false - - Layout.alignment: Qt.AlignVCenter - asynchronous: true active: isHidden || isFav sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) @@ -371,7 +567,6 @@ Item { Component { id: hiddenIcon - MaterialIcon { text: "visibility_off" fill: 1 @@ -381,7 +576,6 @@ Item { Component { id: favouriteIcon - MaterialIcon { text: "favorite" fill: 1 @@ -415,30 +609,11 @@ Item { nextComponent = targetComponent; } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = pane ? (pane.id || pane.entry?.id || "") : ""; - } - - onDisplayedAppChanged: { - if (displayedApp) { - const appId = displayedApp.id || displayedApp.entry?.id; - root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); - root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); - } else { - root.hideFromLauncherChecked = false; - root.favouriteChecked = false; - } - } - Loader { id: rightLauncherLoader - property var displayedApp: rightLauncherPane.displayedApp - anchors.fill: parent - asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -447,6 +622,8 @@ Item { sourceComponent: rightLauncherPane.targetComponent active: true + property var displayedApp: rightLauncherPane.displayedApp + onItemChanged: { if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { rightLauncherPane.displayedApp = rightLauncherPane.pane; @@ -481,6 +658,22 @@ Item { ] } } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); + } else { + root.hideFromLauncherChecked = false; + root.favouriteChecked = false; + } + } } } } @@ -490,7 +683,6 @@ Item { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -514,10 +706,10 @@ Item { ColumnLayout { id: appDetailsLayout + anchors.fill: parent readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - anchors.fill: parent spacing: Appearance.spacing.normal SettingsHeader { @@ -544,8 +736,6 @@ Item { IconImage { id: appIconImage - - asynchronous: true Layout.alignment: Qt.AlignHCenter implicitSize: Appearance.font.size.extraLarge * 3 * 2 source: { @@ -562,7 +752,6 @@ Item { StyledText { id: appTitleText - Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large @@ -580,7 +769,6 @@ Item { StyledFlickable { id: detailsFlickable - anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height @@ -591,7 +779,6 @@ Item { ColumnLayout { id: debugLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -663,6 +850,120 @@ Item { } } } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + visible: appDetailsLayout.displayedApp !== null + title: qsTr("Category") + description: qsTr("Assign this app to a category") + } + + SectionContainer { + visible: appDetailsLayout.displayedApp !== null + + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: Appearance.spacing.small + rowSpacing: Appearance.spacing.small + + Repeater { + model: Config.launcher.categories + + delegate: StyledRect { + required property var modelData + required property int index + + Layout.fillWidth: true + implicitHeight: categoryContent.height + Appearance.padding.normal * 2 + + property string categoryName: modelData.name + property bool isAssigned: { + const app = appDetailsLayout.displayedApp; + if (!app) + return false; + const appId = app.id || app.entry?.id; + return root.appHasCategory(appId, categoryName); + } + + color: isAssigned ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + + StateLayer { + radius: parent.radius + function onClicked(): void { + const app = appDetailsLayout.displayedApp; + if (!app) + return; + const appId = app.id || app.entry?.id; + console.log(`Toggling category ${categoryName} for app ${appId}`); + root.toggleAppCategory(appId, categoryName); + } + } + + Row { + id: categoryContent + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + color: isAssigned ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.weight: isAssigned ? 500 : 400 + color: isAssigned ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: "check" + visible: isAssigned + color: Colours.palette.m3onSecondaryContainer + } + } + } + } + + TextButton { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small + text: qsTr("Clear Category") + inactiveColour: Colours.palette.m3errorContainer + inactiveOnColour: Colours.palette.m3onErrorContainer + visible: { + const app = appDetailsLayout.displayedApp; + if (!app) + return false; + const appId = app.id || app.entry?.id; + return root.getAppCategories(appId).length > 0; + } + + onClicked: { + const app = appDetailsLayout.displayedApp; + if (!app) + return; + const appId = app.id || app.entry?.id; + + // Remove all categories for this app + const newCategories = []; + for (let i = 0; i < Config.launcher.appCategories.length; i++) { + const item = Config.launcher.appCategories[i]; + if (!item || item.appId !== appId) { + newCategories.push(item); + } + } + Config.launcher.appCategories = newCategories; + Config.save(); + } + } + } + } } } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 95fc2fb07..61c904906 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -2,13 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -66,6 +67,135 @@ ColumnLayout { } } + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Categories") + description: qsTr("Manage launcher categories") + } + + SectionContainer { + contentSpacing: Appearance.spacing.smaller + + ToggleRow { + Layout.bottomMargin: Appearance.spacing.normal + label: qsTr("Enable categories") + checked: Config.launcher.enableCategories + toggle.onToggled: { + Config.launcher.enableCategories = checked; + Config.save(); + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("+ Add Category") + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + editCategoryDialog.editIndex = -1; + editCategoryDialog.categoryName = ""; + editCategoryDialog.categoryIcon = ""; + editCategoryDialog.open(); + } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + spacing: Appearance.spacing.smaller + + model: Config.launcher.categories + + delegate: StyledRect { + required property var modelData + required property int index + + width: ListView.view ? ListView.view.width : undefined + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + + implicitHeight: categoryRow.implicitHeight + Appearance.padding.normal * 2 + + RowLayout { + id: categoryRow + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.normal + anchors.bottomMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + MaterialIcon { + text: modelData.icon + color: Colours.palette.m3onSurface + } + + StyledText { + Layout.fillWidth: true + text: modelData.name + color: Colours.palette.m3onSurface + } + + IconButton { + type: IconButton.Tonal + icon: "arrow_upward" + radius: Appearance.rounding.normal + visible: index > 0 + onClicked: { + const categories = [...Config.launcher.categories]; + const temp = categories[index]; + categories[index] = categories[index - 1]; + categories[index - 1] = temp; + Config.launcher.categories = categories; + Config.save(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "arrow_downward" + radius: Appearance.rounding.normal + visible: index < Config.launcher.categories.length - 1 + onClicked: { + const categories = [...Config.launcher.categories]; + const temp = categories[index]; + categories[index] = categories[index + 1]; + categories[index + 1] = temp; + Config.launcher.categories = categories; + Config.save(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "edit" + radius: Appearance.rounding.normal + onClicked: { + editCategoryDialog.editIndex = index; + editCategoryDialog.categoryName = modelData.name; + editCategoryDialog.categoryIcon = modelData.icon; + editCategoryDialog.open(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "delete" + radius: Appearance.rounding.normal + onClicked: { + const categories = [...Config.launcher.categories]; + categories.splice(index, 1); + Config.launcher.categories = categories; + Config.save(); + } + } + } + } + } + } + SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Display") @@ -214,4 +344,102 @@ ColumnLayout { value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) } } + + Popup { + id: editCategoryDialog + + property int editIndex: -1 + property string categoryName: "" + property string categoryIcon: "" + + parent: Overlay.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) + padding: Appearance.padding.large + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: StyledRect { + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.large + } + + contentItem: ColumnLayout { + spacing: Appearance.spacing.normal + + StyledText { + text: editCategoryDialog.editIndex === -1 ? qsTr("Add Category") : qsTr("Edit Category") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Configure category name and icon") + wrapMode: Text.WordWrap + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + TextField { + id: categoryNameField + Layout.fillWidth: true + placeholderText: qsTr("Category name") + text: editCategoryDialog.categoryName + onTextChanged: editCategoryDialog.categoryName = text + } + + TextField { + id: categoryIconField + Layout.fillWidth: true + placeholderText: qsTr("Icon name (e.g., folder, code)") + text: editCategoryDialog.categoryIcon + onTextChanged: editCategoryDialog.categoryIcon = text + } + + Item { Layout.preferredHeight: Appearance.spacing.normal } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + Layout.fillWidth: true + text: qsTr("Cancel") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + + onClicked: editCategoryDialog.close() + } + + TextButton { + Layout.fillWidth: true + text: editCategoryDialog.editIndex === -1 ? qsTr("Add") : qsTr("Save") + enabled: editCategoryDialog.categoryName.length > 0 && editCategoryDialog.categoryIcon.length > 0 + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + const categories = [...Config.launcher.categories]; + const newCategory = { + name: editCategoryDialog.categoryName, + icon: editCategoryDialog.categoryIcon + }; + + if (editCategoryDialog.editIndex === -1) { + categories.push(newCategory); + } else { + categories[editCategoryDialog.editIndex] = newCategory; + } + + Config.launcher.categories = categories; + Config.save(); + editCategoryDialog.close(); + } + } + } + } + } } diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 9b78ccafc..4e60b3d48 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts DeviceDetails { id: root diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 4fcd5b92d..d1eb95798 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts DeviceList { id: root @@ -147,6 +147,8 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + function onClicked(): void { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); @@ -154,8 +156,6 @@ DeviceList { Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } - - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 8fb833fde..59d82bb08 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import Quickshell.Widgets import qs.components import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 3d99c47c2..90bfcf46a 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 15ac56d24..bda7cb18a 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 990c4a269..26cdbfacd 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -120,7 +120,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { VpnList { session: root.session @@ -139,7 +138,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { EthernetList { session: root.session @@ -158,7 +156,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { WirelessList { session: root.session @@ -199,6 +196,9 @@ Item { } Connections { + target: root.session && root.session.vpn ? root.session.vpn : null + enabled: target !== null + function onActiveChanged() { // Clear others when VPN is selected if (root.session && root.session.vpn && root.session.vpn.active) { @@ -209,12 +209,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null } Connections { + target: root.session && root.session.ethernet ? root.session.ethernet : null + enabled: target !== null + function onActiveChanged() { // Clear others when ethernet is selected if (root.session && root.session.ethernet && root.session.ethernet.active) { @@ -225,12 +225,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.ethernet ? root.session.ethernet : null - enabled: target !== null } Connections { + target: root.session && root.session.network ? root.session.network : null + enabled: target !== null + function onActiveChanged() { // Clear others when wireless is selected if (root.session && root.session.network && root.session.network.active) { @@ -241,9 +241,6 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.network ? root.session.network : null - enabled: target !== null } Loader { @@ -281,7 +278,6 @@ Item { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -305,7 +301,6 @@ Item { StyledFlickable { id: ethernetFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height @@ -329,7 +324,6 @@ Item { StyledFlickable { id: wirelessFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: wirelessDetailsInner.height @@ -353,7 +347,6 @@ Item { StyledFlickable { id: vpnFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: vpnDetailsInner.height diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 8d91067c7..1c71cd716 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts DeviceDetails { id: root @@ -201,10 +201,6 @@ DeviceDetails { property string displayName: "" property string interfaceName: "" - function closeWithAnimation(): void { - close(); - } - parent: Overlay.overlay anchors.centerIn: parent width: Math.min(400, parent.width - Appearance.padding.large * 2) @@ -250,6 +246,10 @@ DeviceDetails { } } + function closeWithAnimation(): void { + close(); + } + Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } @@ -302,7 +302,6 @@ DeviceDetails { StyledTextField { id: displayNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -339,7 +338,6 @@ DeviceDetails { StyledTextField { id: interfaceNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 3646841ce..81f4a45a3 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -21,6 +21,7 @@ ColumnLayout { spacing: Appearance.spacing.normal Connections { + target: VPN function onConnectedChanged() { if (!VPN.connected && root.pendingSwitchIndex >= 0) { const targetIndex = root.pendingSwitchIndex; @@ -49,8 +50,6 @@ ColumnLayout { }); } } - - target: VPN } TextButton { @@ -98,7 +97,6 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -183,6 +181,7 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { + enabled: !VPN.connecting function onClicked(): void { const clickedIndex = modelData.index; @@ -217,8 +216,6 @@ ColumnLayout { } } } - - enabled: !VPN.connecting } MaterialIcon { @@ -259,6 +256,8 @@ ColumnLayout { } } } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -272,43 +271,6 @@ ColumnLayout { property string displayName: "" property string interfaceName: "" - function showProviderSelection(): void { - currentState = "selection"; - open(); - } - - function closeWithAnimation(): void { - close(); - } - - function showAddForm(providerType: string, defaultDisplayName: string): void { - editIndex = -1; - providerName = providerType; - displayName = defaultDisplayName; - interfaceName = ""; - - if (currentState === "selection") { - transitionToForm.start(); - } else { - currentState = "form"; - isClosing = false; - open(); - } - } - - function showEditForm(index: int): void { - const provider = Config.utilities.vpn.provider[index]; - const isObject = typeof provider === "object"; - - editIndex = index; - providerName = isObject ? (provider.name || "custom") : String(provider); - displayName = isObject ? (provider.displayName || providerName) : providerName; - interfaceName = isObject ? (provider.interface || "") : ""; - - currentState = "form"; - open(); - } - parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) @@ -359,6 +321,43 @@ ColumnLayout { } } + function showProviderSelection(): void { + currentState = "selection"; + open(); + } + + function closeWithAnimation(): void { + close(); + } + + function showAddForm(providerType: string, defaultDisplayName: string): void { + editIndex = -1; + providerName = providerType; + displayName = defaultDisplayName; + interfaceName = ""; + + if (currentState === "selection") { + transitionToForm.start(); + } else { + currentState = "form"; + isClosing = false; + open(); + } + } + + function showEditForm(index: int): void { + const provider = Config.utilities.vpn.provider[index]; + const isObject = typeof provider === "object"; + + editIndex = index; + providerName = isObject ? (provider.name || "custom") : String(provider); + displayName = isObject ? (provider.displayName || providerName) : providerName; + interfaceName = isObject ? (provider.interface || "") : ""; + + currentState = "form"; + open(); + } + Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) } @@ -367,6 +366,36 @@ ColumnLayout { currentState = "selection"; } + SequentialAnimation { + id: transitionToForm + + ParallelAnimation { + Anim { + target: selectionContent + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + ScriptAction { + script: { + vpnDialog.currentState = "form"; + } + } + + ParallelAnimation { + Anim { + target: formContent + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.large @@ -557,7 +586,6 @@ ColumnLayout { StyledTextField { id: displayNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -594,7 +622,6 @@ ColumnLayout { StyledTextField { id: interfaceNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -655,35 +682,5 @@ ColumnLayout { } } } - - SequentialAnimation { - id: transitionToForm - - ParallelAnimation { - Anim { - target: selectionContent - property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - ScriptAction { - script: { - vpnDialog.currentState = "form"; - } - } - - ParallelAnimation { - Anim { - target: formContent - property: "opacity" - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index ae689050e..49d801d9a 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -82,7 +82,6 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.normal @@ -148,6 +147,8 @@ ColumnLayout { } } } + + implicitHeight: 60 } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 913e5d75a..e8777cdf7 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts DeviceDetails { id: root @@ -19,12 +19,66 @@ DeviceDetails { required property Session session readonly property var network: root.session.network.active + device: network + + Component.onCompleted: { + updateDeviceDetails(); + checkSavedProfile(); + } + + onNetworkChanged: { + connectionUpdateTimer.stop(); + if (network && network.ssid) { + connectionUpdateTimer.start(); + } + updateDeviceDetails(); + checkSavedProfile(); + } + function checkSavedProfile(): void { if (network && network.ssid) { Nmcli.loadSavedConnections(() => {}); } } + Connections { + target: Nmcli + function onActiveChanged() { + updateDeviceDetails(); + } + function onWirelessDeviceDetailsChanged() { + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { + connectionUpdateTimer.stop(); + } + } + } + } + + Timer { + id: connectionUpdateTimer + interval: 500 + repeat: true + running: network && network.ssid + onTriggered: { + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { + Nmcli.getWirelessDeviceDetails("", () => {}); + } else { + connectionUpdateTimer.stop(); + } + } else { + if (Nmcli.wirelessDeviceDetails !== null) { + Nmcli.wirelessDeviceDetails = null; + } + } + } + } + } + function updateDeviceDetails(): void { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); @@ -38,22 +92,6 @@ DeviceDetails { } } - device: network - - Component.onCompleted: { - updateDeviceDetails(); - checkSavedProfile(); - } - - onNetworkChanged: { - connectionUpdateTimer.stop(); - if (network && network.ssid) { - connectionUpdateTimer.start(); - } - updateDeviceDetails(); - checkSavedProfile(); - } - headerComponent: Component { ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" @@ -170,44 +208,4 @@ DeviceDetails { } } ] - - Connections { - function onActiveChanged() { - updateDeviceDetails(); - } - function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - connectionUpdateTimer.stop(); - } - } - } - - target: Nmcli - } - - Timer { - id: connectionUpdateTimer - - interval: 500 - repeat: true - running: network && network.ssid - onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - Nmcli.getWirelessDeviceDetails("", () => {}); - } else { - connectionUpdateTimer.stop(); - } - } else { - if (Nmcli.wirelessDeviceDetails !== null) { - Nmcli.wirelessDeviceDetails = null; - } - } - } - } - } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index bff142d3a..57a155fd7 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -3,28 +3,22 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts DeviceList { id: root required property Session session - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } - title: qsTr("Networks (%1)").arg(Nmcli.networks.length) description: qsTr("All available WiFi networks") activeItem: session.network.active @@ -110,7 +104,6 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -215,6 +208,8 @@ DeviceList { } } } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } @@ -224,4 +219,10 @@ DeviceList { checkSavedProfileForNetwork(item.ssid); } } + + function checkSavedProfileForNetwork(ssid: string): void { + if (ssid && ssid.length > 0) { + Nmcli.loadSavedConnections(() => {}); + } + } } diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index cccf8222c..8150af9cf 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import Quickshell.Widgets import qs.components import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 8700f1c43..7ad5204a4 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -29,47 +29,6 @@ Item { } property bool isClosing: false - - function checkConnectionStatus(): void { - if (!root.visible || !connectButton.connecting) { - return; - } - - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - connectionSuccessTimer.start(); - return; - } - - if (Nmcli.pendingConnection === null && connectButton.connecting) { - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - } - visible: session.network.showPasswordDialog || isClosing enabled: session.network.showPasswordDialog && !isClosing focus: enabled @@ -105,7 +64,6 @@ Item { color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 - Keys.onEscapePressed: closeDialog() Behavior on opacity { Anim {} @@ -136,6 +94,8 @@ Item { } } + Keys.onEscapePressed: closeDialog() + ColumnLayout { id: content @@ -190,23 +150,16 @@ Item { Item { id: passwordContainer - - property string passwordBuffer: "" - Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + focus: true Keys.onPressed: event => { if (!activeFocus) { forceActiveFocus(); } - if (event.key === Qt.Key_Escape) { - event.accepted = false; - closeDialog(); - } - if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } @@ -224,16 +177,15 @@ Item { } event.accepted = true; } else if (event.text && event.text.length > 0) { - if (event.key === Qt.Key_Tab) { - event.accepted = false; - return; - } passwordBuffer += event.text; event.accepted = true; } } + property string passwordBuffer: "" + Connections { + target: root.session.network function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { Qt.callLater(() => { @@ -243,11 +195,10 @@ Item { }); } } - - target: root.session.network } Connections { + target: root function onVisibleChanged(): void { if (root.visible) { Qt.callLater(() => { @@ -255,8 +206,6 @@ Item { }); } } - - target: root } StyledRect { @@ -288,17 +237,16 @@ Item { } StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + function onClicked(): void { passwordContainer.forceActiveFocus(); } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor } StyledText { id: placeholder - anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline @@ -466,14 +414,40 @@ Item { } } - Timer { - id: connectionMonitor + function checkConnectionStatus(): void { + if (!root.visible || !connectButton.connecting) { + return; + } - property int repeatCount: 0 + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (isConnected) { + connectionSuccessTimer.start(); + return; + } + + if (Nmcli.pendingConnection === null && connectButton.connecting) { + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + Timer { + id: connectionMonitor interval: 1000 repeat: true triggeredOnStart: false + property int repeatCount: 0 + onTriggered: { repeatCount++; checkConnectionStatus(); @@ -488,7 +462,6 @@ Item { Timer { id: connectionSuccessTimer - interval: 500 onTriggered: { if (root.visible && Nmcli.active && Nmcli.active.ssid) { @@ -504,6 +477,7 @@ Item { } Connections { + target: Nmcli function onActiveChanged() { if (root.visible) { checkConnectionStatus(); @@ -520,7 +494,18 @@ Item { Nmcli.forgetNetwork(ssid); } } + } - target: Nmcli + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); } } diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 7b929bf98..b4eb391d4 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml index 9b16bfe59..8678672df 100644 --- a/modules/controlcenter/state/BluetoothState.qml +++ b/modules/controlcenter/state/BluetoothState.qml @@ -1,5 +1,5 @@ -import QtQuick import Quickshell.Bluetooth +import QtQuick QtObject { id: root diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d61e932dc..ba65c1e74 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -22,8 +22,6 @@ Item { property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true - property bool clockBackground: Config.bar.clock.background ?? false - property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 @@ -53,11 +51,24 @@ Item { property list monitorNames: Hypr.monitorNames() property list excludedScreens: Config.bar.excludedScreens ?? [] + anchors.fill: parent + + Component.onCompleted: { + if (Config.bar.entries) { + entriesModel.clear(); + for (let i = 0; i < Config.bar.entries.length; i++) { + const entry = Config.bar.entries[i]; + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } + } + function saveConfig(entryIndex, entryEnabled) { Config.bar.activeWindow.compact = root.activeWindowCompact; Config.bar.activeWindow.inverted = root.activeWindowInverted; - Config.bar.clock.background = root.clockBackground; - Config.bar.clock.showDate = root.clockShowDate; Config.bar.clock.showIcon = root.clockShowIcon; Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; @@ -103,28 +114,12 @@ Item { Config.save(); } - anchors.fill: parent - - Component.onCompleted: { - if (Config.bar.entries) { - entriesModel.clear(); - for (let i = 0; i < Config.bar.entries.length; i++) { - const entry = Config.bar.entries[i]; - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } - } - } - ListModel { id: entriesModel } ClippingRectangle { id: taskbarClippingRect - anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -141,14 +136,12 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large - asynchronous: true sourceComponent: taskbarContentComponent } } InnerBorder { id: taskbarBorder - leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -158,7 +151,6 @@ Item { StyledFlickable { id: sidebarFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -168,7 +160,6 @@ Item { ColumnLayout { id: sidebarLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -268,13 +259,11 @@ Item { RowLayout { id: mainRowLayout - Layout.fillWidth: true spacing: Appearance.spacing.normal ColumnLayout { id: leftColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -300,7 +289,6 @@ Item { RowLayout { id: workspacesShownRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -336,7 +324,6 @@ Item { RowLayout { id: workspacesActiveIndicatorRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -370,7 +357,6 @@ Item { RowLayout { id: workspacesOccupiedBgRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -404,7 +390,6 @@ Item { RowLayout { id: workspacesShowWindowsRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -438,7 +423,6 @@ Item { RowLayout { id: workspacesMaxWindowIconsRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -474,7 +458,6 @@ Item { RowLayout { id: workspacesPerMonitorRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -541,7 +524,6 @@ Item { ColumnLayout { id: middleColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -555,24 +537,6 @@ Item { font.pointSize: Appearance.font.size.normal } - SwitchRow { - label: qsTr("Background") - checked: root.clockBackground - onToggled: checked => { - root.clockBackground = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show date") - checked: root.clockShowDate - onToggled: checked => { - root.clockShowDate = checked; - root.saveConfig(); - } - } - SwitchRow { label: qsTr("Show clock icon") checked: root.clockShowIcon @@ -667,7 +631,6 @@ Item { ColumnLayout { id: rightColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/vpn/VpnDetails.qml b/modules/controlcenter/vpn/VpnDetails.qml new file mode 100644 index 000000000..e69de29bb diff --git a/modules/controlcenter/vpn/VpnList.qml b/modules/controlcenter/vpn/VpnList.qml new file mode 100644 index 000000000..e69de29bb From 48b60b18625ea31b3a513e581d5d7acd4d7c5f47 Mon Sep 17 00:00:00 2001 From: AleksElixir <71710534+AleksElixir@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:53:31 +0200 Subject: [PATCH 3/4] Add files via upload --- modules/BatteryMonitor.qml | 14 +- modules/IdleMonitors.qml | 6 +- modules/Shortcuts.qml | 37 +- modules/dashboard/Background.qml | 4 +- modules/dashboard/Content.qml | 49 +- modules/dashboard/Dash.qml | 11 +- modules/dashboard/Media.qml | 248 ++----- modules/dashboard/Performance.qml | 10 +- modules/dashboard/Tabs.qml | 24 +- modules/dashboard/Weather.qml | 280 ++++++++ modules/dashboard/Wrapper.qml | 16 +- modules/dashboard/dash/Calendar.qml | 38 +- modules/dashboard/dash/DateTime.qml | 5 +- modules/dashboard/dash/Media.qml | 33 +- modules/dashboard/dash/Resources.qml | 2 +- modules/dashboard/dash/User.qml | 13 +- modules/dashboard/dash/Weather.qml | 57 ++ modules/drawers/Backgrounds.qml | 31 +- modules/drawers/Border.qml | 4 +- modules/drawers/Drawers.qml | 47 +- modules/drawers/Exclusions.qml | 7 +- modules/drawers/Interactions.qml | 35 +- modules/drawers/Panels.qml | 90 ++- modules/launcher/AppList.qml | 192 +++++- modules/launcher/Background.qml | 4 +- modules/launcher/Content.qml | 138 +++- modules/launcher/ContentList.qml | 16 +- modules/launcher/WallpaperList.qml | 8 +- modules/launcher/Wrapper.qml | 57 +- modules/launcher/items/ActionItem.qml | 7 +- modules/launcher/items/AppItem.qml | 47 +- modules/launcher/items/CalcItem.qml | 25 +- modules/launcher/items/SchemeItem.qml | 9 +- modules/launcher/items/VariantItem.qml | 9 +- modules/launcher/items/WallpaperItem.qml | 13 +- .../items/contextmenu/ActionRegistry.qml | 222 ++++++ .../items/contextmenu/ContextMenu.qml | 640 ++++++++++++++++++ .../launcher/items/contextmenu/MenuItem.qml | 113 ++++ .../items/contextmenu/MenuItemFactory.qml | 135 ++++ .../launcher/items/contextmenu/Submenus.qml | 197 ++++++ .../items/contextmenu/shaders/goo_sdf.frag | 105 +++ .../contextmenu/shaders/goo_sdf.frag.qsb | Bin 0 -> 5379 bytes .../items/contextmenu/shaders/goo_sdf.vert | 27 + .../contextmenu/shaders/goo_sdf.vert.qsb | Bin 0 -> 1888 bytes modules/launcher/services/Actions.qml | 4 +- modules/launcher/services/Apps.qml | 4 +- modules/launcher/services/M3Variants.qml | 4 +- modules/launcher/services/Schemes.qml | 8 +- modules/lock/Center.qml | 19 +- modules/lock/Content.qml | 4 +- modules/lock/Fetch.qml | 7 +- modules/lock/InputField.qml | 10 +- modules/lock/Lock.qml | 10 +- modules/lock/LockSurface.qml | 10 +- modules/lock/Media.qml | 28 +- modules/lock/NotifDock.qml | 9 +- modules/lock/NotifGroup.qml | 19 +- modules/lock/Pam.qml | 14 +- modules/lock/Resources.qml | 4 +- modules/lock/WeatherInfo.qml | 8 +- modules/notifications/Background.qml | 4 +- modules/notifications/Content.qml | 186 +++-- modules/notifications/Notification.qml | 33 +- modules/notifications/Wrapper.qml | 11 +- modules/osd/Background.qml | 4 +- modules/osd/Content.qml | 25 +- modules/osd/Wrapper.qml | 14 +- modules/session/Background.qml | 4 +- modules/session/Content.qml | 16 +- modules/session/Wrapper.qml | 5 +- modules/sidebar/Background.qml | 4 +- modules/sidebar/Content.qml | 6 +- modules/sidebar/Notif.qml | 59 +- modules/sidebar/NotifActionList.qml | 11 +- modules/sidebar/NotifDock.qml | 25 +- modules/sidebar/NotifDockList.qml | 208 +++--- modules/sidebar/NotifGroup.qml | 16 +- modules/sidebar/NotifGroupList.qml | 260 ++++--- modules/sidebar/Wrapper.qml | 4 +- modules/utilities/Background.qml | 4 +- modules/utilities/Content.qml | 8 +- modules/utilities/RecordingDeleteModal.qml | 9 +- modules/utilities/Wrapper.qml | 10 +- modules/utilities/cards/IdleInhibit.qml | 5 +- modules/utilities/cards/Record.qml | 7 +- modules/utilities/cards/RecordingList.qml | 16 +- modules/utilities/cards/Toggles.qml | 31 +- modules/utilities/toasts/ToastItem.qml | 6 +- modules/utilities/toasts/Toasts.qml | 6 +- modules/windowinfo/Buttons.qml | 49 +- modules/windowinfo/Details.qml | 6 +- modules/windowinfo/Preview.qml | 13 +- modules/windowinfo/WindowInfo.qml | 8 +- 93 files changed, 3114 insertions(+), 1146 deletions(-) create mode 100644 modules/dashboard/Weather.qml create mode 100644 modules/dashboard/dash/Weather.qml create mode 100644 modules/launcher/items/contextmenu/ActionRegistry.qml create mode 100644 modules/launcher/items/contextmenu/ContextMenu.qml create mode 100644 modules/launcher/items/contextmenu/MenuItem.qml create mode 100644 modules/launcher/items/contextmenu/MenuItemFactory.qml create mode 100644 modules/launcher/items/contextmenu/Submenus.qml create mode 100644 modules/launcher/items/contextmenu/shaders/goo_sdf.frag create mode 100644 modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb create mode 100644 modules/launcher/items/contextmenu/shaders/goo_sdf.vert create mode 100644 modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 0596a1aa6..d24cff274 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -1,8 +1,8 @@ -import QtQuick +import qs.config +import Caelestia import Quickshell import Quickshell.Services.UPower -import Caelestia -import qs.config +import QtQuick Scope { id: root @@ -10,6 +10,8 @@ Scope { readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { + target: UPower + function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) @@ -21,11 +23,11 @@ Scope { level.warned = false; } } - - target: UPower } Connections { + target: UPower.displayDevice + function onPercentageChanged(): void { if (!UPower.onBattery) return; @@ -43,8 +45,6 @@ Scope { hibernateTimer.start(); } } - - target: UPower.displayDevice } Timer { diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index 440dc704d..b7ce05843 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "lock" +import qs.config +import qs.services +import Caelestia.Internal import Quickshell import Quickshell.Wayland -import Caelestia.Internal -import qs.services -import qs.config Scope { id: root diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index d73e63511..3bf20a4f6 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,9 +1,9 @@ -import Quickshell -import Quickshell.Io -import Caelestia import qs.components.misc -import qs.services import qs.modules.controlcenter +import qs.services +import Caelestia +import Quickshell +import Quickshell.Io Scope { id: root @@ -11,17 +11,13 @@ Scope { property bool launcherInterrupted readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "controlCenter" description: "Open control center" onPressed: WindowFactory.create() } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "showall" description: "Toggle launcher, dashboard and osd" onPressed: { @@ -32,9 +28,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "dashboard" description: "Toggle dashboard" onPressed: { @@ -45,9 +39,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "session" description: "Toggle session menu" onPressed: { @@ -58,9 +50,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "launcher" description: "Toggle launcher" onPressed: root.launcherInterrupted = false @@ -73,17 +63,14 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "launcherInterrupt" description: "Interrupt launcher keybind" onPressed: root.launcherInterrupted = true } - // qmllint disable unresolved-type + CustomShortcut { - // qmllint enable unresolved-type name: "sidebar" description: "Toggle sidebar" onPressed: { @@ -94,9 +81,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "utilities" description: "Toggle utilities" onPressed: { @@ -108,6 +93,8 @@ Scope { } IpcHandler { + target: "drawers" + function toggle(drawer: string): void { if (list().split("\n").includes(drawer)) { if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) @@ -123,19 +110,19 @@ Scope { const visibilities = Visibilities.getForActive(); return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); } - - target: "drawers" } IpcHandler { + target: "controlCenter" + function open(): void { WindowFactory.create(); } - - target: "controlCenter" } IpcHandler { + target: "toaster" + function info(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Info); } @@ -151,7 +138,5 @@ Scope { function error(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Error); } - - target: "toaster" } } diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml index c6223eb6c..e2a91f741 100644 --- a/modules/dashboard/Background.qml +++ b/modules/dashboard/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 5b3bbbe6b..bbb42724c 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -1,27 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.filedialog import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i) as Loader; - if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) - return true; - } - return false; - } - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker readonly property var dashboardTabs: { @@ -90,24 +81,21 @@ Item { id: view readonly property int currentIndex: root.state.currentTab - readonly property Item currentItem: { - repeater.count; // Trigger update on count change - return repeater.itemAt(currentIndex); - } + readonly property Item currentItem: row.children[currentIndex] anchors.fill: parent flickableDirection: Flickable.HorizontalFlick - implicitWidth: currentItem?.implicitWidth ?? 0 - implicitHeight: currentItem?.implicitHeight ?? 0 + implicitWidth: currentItem.implicitWidth + implicitHeight: currentItem.implicitHeight - contentX: currentItem?.x ?? 0 + contentX: currentItem.x contentWidth: row.implicitWidth contentHeight: row.implicitHeight onContentXChanged: { - if (!moving || !currentItem) + if (!moving) return; const x = contentX - currentItem.x; @@ -118,24 +106,19 @@ Item { } onDragEnded: { - if (!currentItem) - return; - const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); else - contentX = Qt.binding(() => currentItem?.x ?? 0); + contentX = Qt.binding(() => currentItem.x); } RowLayout { id: row Repeater { - id: repeater - model: ScriptModel { values: root.dashboardTabs } @@ -163,7 +146,6 @@ Item { Component { id: dashComponent - Dash { visibilities: root.visibilities state: root.state @@ -173,22 +155,19 @@ Item { Component { id: mediaComponent - - MediaWrapper { + Media { visibilities: root.visibilities } } Component { id: performanceComponent - Performance {} } Component { id: weatherComponent - - WeatherTab {} + Weather {} } Behavior on contentX { diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index c0657f58e..71e224fbe 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -1,15 +1,16 @@ -import "dash" -import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services import qs.config +import "dash" +import Quickshell +import QtQuick.Layouts GridLayout { id: root - required property DrawerVisibilities visibilities - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker rowSpacing: Appearance.spacing.normal @@ -40,7 +41,7 @@ GridLayout { radius: Appearance.rounding.large * 1.5 - SmallWeather {} + Weather {} } Rect { diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 490461851..722bc9332 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,29 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Services.Mpris -import Caelestia.Services import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils +import qs.config +import Caelestia.Services +import Quickshell +import Quickshell.Services.Mpris +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: lyricMenuOpen - - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 - readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight - - property bool lyricMenuOpen: false - property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 - property bool lyricsShowingDebounced: false + required property PersistentProperties visibilities property real playerProgress: { const active = Players.active; @@ -43,21 +35,8 @@ Item { return `${mins}:${secs}`; } - onLyricsShowingChanged: { - if (lyricsShowing) { - lyricsHideDelay.stop(); - lyricsShowingDebounced = true; - } else { - lyricsHideDelay.restart(); - } - } - implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 - implicitHeight: nonAnimHeight - - Behavior on implicitHeight { - Anim {} - } + implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 Behavior on playerProgress { Anim { @@ -70,27 +49,7 @@ Item { interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true - onTriggered: { - if (!Players.active) - return; - LyricsService.updatePosition(); - Players.active?.positionChanged(); - } - } - - Timer { - id: lyricsHideDelay - - interval: 300 - repeat: false - } - - Connections { - function onTriggered() { - root.lyricsShowingDebounced = false; - } - - target: lyricsHideDelay + onTriggered: Players.active?.positionChanged() } ServiceRef { @@ -186,13 +145,6 @@ Item { fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height - - MouseArea { - anchors.fill: parent - onClicked: { - LyricsService.toggleVisibility(); - } - } } } @@ -248,13 +200,6 @@ Item { wrapMode: Players.active ? Text.NoWrap : Text.WordWrap } - LyricsView { - id: lyricsViewInDetails - - Layout.fillWidth: true - Layout.preferredHeight: 200 - } - RowLayout { id: controls @@ -264,14 +209,6 @@ Item { spacing: Appearance.spacing.small - PlayerControl { - type: IconButton.Text - icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" - font.pointSize: Math.round(Appearance.font.size.large) - disabled: !Players.active?.shuffleSupported - onClicked: Players.active.shuffle = !Players.active?.shuffle - } - PlayerControl { type: IconButton.Text icon: "skip_previous" @@ -298,13 +235,6 @@ Item { disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } - - PlayerControl { - type: IconButton.Text - icon: "lyrics" - font.pointSize: Math.round(Appearance.font.size.large) - onClicked: root.lyricMenuOpen = !root.lyricMenuOpen - } } StyledSlider { @@ -328,6 +258,9 @@ Item { } CustomMouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) @@ -339,9 +272,6 @@ Item { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } - - anchors.fill: parent - acceptedButtons: Qt.NoButton } } @@ -369,123 +299,83 @@ Item { font.pointSize: Appearance.font.size.small } } - } - - ColumnLayout { - id: leftSection - - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 - anchors.left: details.right - anchors.leftMargin: Appearance.spacing.normal - visible: lyricMenu.height === 0 || opacity > 0 - opacity: lyricMenu.height === 0 ? 1 : 0 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small - Behavior on opacity { - NumberAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.OutCubic + PlayerControl { + type: IconButton.Text + icon: "move_up" + inactiveOnColour: Colours.palette.m3secondary + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canRaise + onClicked: { + Players.active?.raise(); + root.visibilities.dashboard = false; + } } - } - Item { - id: bongocat + SplitButton { + id: playerSelector - implicitWidth: visualiser.width - implicitHeight: visualiser.height + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - AnimatedImage { - anchors.centerIn: parent + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 + label.Layout.maximumWidth: slider.implicitWidth * 0.28 + label.elide: Text.ElideRight - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit - } - } - } + stateLayer.disabled: true + menuOnTop: true - LyricMenu { - id: lyricMenu + Variants { + id: playerList - anchors.top: parent.top - anchors.left: details.right - anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.normal - - contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight + model: Players.list - visible: root.lyricMenuOpen || height > 0 - height: root.lyricMenuOpen ? implicitHeight : 0 - clip: true - - Behavior on height { - NumberAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.OutCubic + PlayerItem {} + } } - } - } - - RowLayout { - id: playerChanger - - parent: !root.lyricsShowingDebounced ? details : leftSection - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; + PlayerControl { + type: IconButton.Text + icon: "delete" + inactiveOnColour: Colours.palette.m3error + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canQuit + onClicked: Players.active?.quit() } } + } - SplitButton { - id: playerSelector - - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") - - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + Item { + id: bongocat - stateLayer.disabled: true - menuOnTop: true + anchors.verticalCenter: parent.verticalCenter + anchors.left: details.right + anchors.leftMargin: Appearance.spacing.normal - Variants { - id: playerList + implicitWidth: visualiser.width + implicitHeight: visualiser.height - model: Players.list + AnimatedImage { + anchors.centerIn: parent - PlayerItem {} - } - } + width: visualiser.width * 0.75 + height: visualiser.height * 0.75 - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() + playing: Players.active?.isPlaying ?? false + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + source: Paths.absolutePath(Config.paths.mediaGif) + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit } } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 8d8d2c016..339c731fc 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -5,8 +5,8 @@ import Quickshell.Services.UPower import Caelestia.Internal import qs.components import qs.components.misc -import qs.services import qs.config +import qs.services Item { id: root @@ -685,22 +685,22 @@ Item { property real smoothMax: targetMax anchors.fill: parent - line1: NetworkUsage.uploadBuffer // qmllint disable missing-type + line1: NetworkUsage.uploadBuffer line1Color: Colours.palette.m3secondary line1FillAlpha: 0.15 - line2: NetworkUsage.downloadBuffer // qmllint disable missing-type + line2: NetworkUsage.downloadBuffer line2Color: Colours.palette.m3tertiary line2FillAlpha: 0.2 maxValue: smoothMax historyLength: NetworkUsage.historyLength Connections { + target: NetworkUsage.downloadBuffer + function onValuesChanged(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } - - target: NetworkUsage.downloadBuffer } NumberAnimation { diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index adeab928d..6e09e767a 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls Item { id: root required property real nonAnimWidth - required property DashboardState state + required property PersistentProperties state required property var tabs readonly property alias count: bar.count @@ -111,13 +111,6 @@ Item { contentItem: CustomMouseArea { id: mouse - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); - else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); - } - implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height @@ -136,6 +129,13 @@ Item { rippleAnim.restart(); } + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); + else if (event.angleDelta.y > 0) + root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + } + SequentialAnimation { id: rippleAnim diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/Weather.qml new file mode 100644 index 000000000..3981633ac --- /dev/null +++ b/modules/dashboard/Weather.qml @@ -0,0 +1,280 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 + implicitHeight: layout.implicitHeight + + readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + + Component.onCompleted: Weather.reload() + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.smaller + + RowLayout { + Layout.leftMargin: Appearance.padding.large + Layout.rightMargin: Appearance.padding.large + Layout.fillWidth: true + + Column { + spacing: Appearance.spacing.small / 2 + + StyledText { + text: Weather.city || qsTr("Loading...") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 + color: Colours.palette.m3onSurface + } + + StyledText { + text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } + + Item { + Layout.fillWidth: true + } + + Row { + spacing: Appearance.spacing.large + + WeatherStat { + icon: "wb_twilight" + label: "Sunrise" + value: Weather.sunrise + colour: Colours.palette.m3tertiary + } + + WeatherStat { + icon: "bedtime" + label: "Sunset" + value: Weather.sunset + colour: Colours.palette.m3tertiary + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.large * 2 + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: bigInfoRow + + anchors.centerIn: parent + spacing: Appearance.spacing.large + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: Weather.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + color: Colours.palette.m3secondary + animate: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + spacing: -Appearance.spacing.small + + StyledText { + text: Weather.temp + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + color: Colours.palette.m3primary + } + + StyledText { + Layout.leftMargin: Appearance.padding.small + text: Weather.description + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + DetailCard { + icon: "water_drop" + label: "Humidity" + value: Weather.humidity + "%" + colour: Colours.palette.m3secondary + } + DetailCard { + icon: "thermostat" + label: "Feels Like" + value: Weather.feelsLike + colour: Colours.palette.m3primary + } + DetailCard { + icon: "air" + label: "Wind" + value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--" + colour: Colours.palette.m3tertiary + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + Layout.leftMargin: Appearance.padding.normal + visible: forecastRepeater.count > 0 + text: qsTr("7-Day Forecast") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3onSurface + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + id: forecastRepeater + + model: Weather.forecast + + StyledRect { + id: forecastItem + + required property int index + required property var modelData + + Layout.fillWidth: true + implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: forecastItemColumn + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: -Appearance.spacing.small / 2 + Layout.alignment: Qt.AlignHCenter + text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") + font.pointSize: Appearance.font.size.small + opacity: 0.7 + color: Colours.palette.m3onSurfaceVariant + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.modelData.icon + font.pointSize: Appearance.font.size.extraLarge + color: Colours.palette.m3secondary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + font.weight: 600 + color: Colours.palette.m3tertiary + } + } + } + } + } + } + + component DetailCard: StyledRect { + id: detailRoot + + property string icon + property string label + property string value + property color colour + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Row { + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + text: detailRoot.icon + color: detailRoot.colour + font.pointSize: Appearance.font.size.large + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: detailRoot.label + font.pointSize: Appearance.font.size.smaller + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + StyledText { + text: detailRoot.value + font.weight: 600 + horizontalAlignment: Text.AlignLeft + } + } + } + } + + component WeatherStat: Row { + id: weatherStat + + property string icon + property string label + property string value + property color colour + + spacing: Appearance.spacing.small + + MaterialIcon { + text: weatherStat.icon + font.pointSize: Appearance.font.size.extraLarge + color: weatherStat.colour + } + + Column { + StyledText { + text: weatherStat.label + font.pointSize: Appearance.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + StyledText { + text: weatherStat.value + font.pointSize: Appearance.font.size.small + font.weight: 600 + color: Colours.palette.m3onSurface + } + } + } +} diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 596c21b75..0e37909e8 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -1,19 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Caelestia import qs.components import qs.components.filedialog import qs.config import qs.utils +import Caelestia +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false - readonly property DashboardState dashState: DashboardState { + required property PersistentProperties visibilities + readonly property PersistentProperties dashState: PersistentProperties { + property int currentTab + property date currentDate: new Date() + reloadableId: "dashboardState" } readonly property FileDialog facePicker: FileDialog { @@ -28,7 +30,7 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 visible: height > 0 implicitHeight: 0 diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index bed3c4d34..56c04938c 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.effects +import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts CustomMouseArea { id: root @@ -17,13 +17,6 @@ CustomMouseArea { readonly property int currMonth: state.currentDate.getMonth() readonly property int currYear: state.currentDate.getFullYear() - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - anchors.left: parent.left anchors.right: parent.right implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 @@ -31,6 +24,13 @@ CustomMouseArea { acceptedButtons: Qt.MiddleButton onClicked: root.state.currentDate = new Date() + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y > 0) + root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + else if (event.angleDelta.y < 0) + root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } + ColumnLayout { id: inner @@ -51,11 +51,11 @@ CustomMouseArea { StateLayer { id: prevMonthStateLayer + radius: Appearance.rounding.full + function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } - - radius: Appearance.rounding.full } MaterialIcon { @@ -76,10 +76,6 @@ CustomMouseArea { implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 StateLayer { - function onClicked(): void { - root.state.currentDate = new Date(); - } - anchors.fill: monthYearDisplay anchors.margins: -Appearance.padding.small anchors.leftMargin: -Appearance.padding.normal @@ -90,6 +86,10 @@ CustomMouseArea { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } + + function onClicked(): void { + root.state.currentDate = new Date(); + } } StyledText { @@ -111,11 +111,11 @@ CustomMouseArea { StateLayer { id: nextMonthStateLayer + radius: Appearance.rounding.full + function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } - - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 7f786a198..e74044883 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -48,7 +48,6 @@ Item { } Loader { - asynchronous: true Layout.alignment: Qt.AlignHCenter active: Config.services.useTwelveHourClock diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 1a4f0c938..d65066954 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Shapes -import Caelestia.Services import qs.components import qs.services import qs.config import qs.utils +import Caelestia.Services +import QtQuick +import QtQuick.Shapes Item { id: root @@ -174,30 +174,30 @@ Item { spacing: Appearance.spacing.small Control { + icon: "skip_previous" + canUse: Players.active?.canGoPrevious ?? false + function onClicked(): void { Players.active?.previous(); } - - icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false } Control { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + canUse: Players.active?.canTogglePlaying ?? false + function onClicked(): void { Players.active?.togglePlaying(); } - - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false } Control { + icon: "skip_next" + canUse: Players.active?.canGoNext ?? false + function onClicked(): void { Players.active?.next(); } - - icon: "skip_next" - canUse: Players.active?.canGoNext ?? false } } @@ -213,7 +213,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit @@ -224,7 +224,6 @@ Item { required property string icon required property bool canUse - function onClicked(): void { } @@ -232,12 +231,12 @@ Item { implicitHeight: implicitWidth StateLayer { + disabled: !control.canUse + radius: Appearance.rounding.full + function onClicked(): void { control.onClicked(); } - - disabled: !control.canUse - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index f5ac45569..7f44a9d0b 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,8 +1,8 @@ -import QtQuick import qs.components import qs.components.misc import qs.services import qs.config +import QtQuick Row { id: root diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 5fb71effa..5ede24bb5 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,17 +1,18 @@ -import QtQuick import qs.components import qs.components.effects -import qs.components.filedialog import qs.components.images +import qs.components.filedialog import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Row { id: root - required property DrawerVisibilities visibilities - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker padding: Appearance.padding.large @@ -70,12 +71,12 @@ Row { opacity: parent.containsMouse ? 1 : 0 StateLayer { + color: Colours.palette.m3onPrimary + function onClicked(): void { root.visibilities.launcher = false; root.facePicker.open(); } - - color: Colours.palette.m3onPrimary } MaterialIcon { diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/Weather.qml new file mode 100644 index 000000000..c90ccf0a4 --- /dev/null +++ b/modules/dashboard/dash/Weather.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import qs.utils +import QtQuick + +Item { + id: root + + anchors.centerIn: parent + + implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + + Component.onCompleted: Weather.reload() + + MaterialIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + animate: true + text: Weather.icon + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + Column { + id: info + + anchors.verticalCenter: parent.verticalCenter + anchors.left: icon.right + anchors.leftMargin: Appearance.spacing.large + + spacing: Appearance.spacing.small + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.temp + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.description + + elide: Text.ElideRight + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) + } + } +} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index b79cd9195..7fa2ca176 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -1,14 +1,15 @@ -import QtQuick -import QtQuick.Shapes +import qs.services import qs.config -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications import qs.modules.osd as Osd +import qs.modules.notifications as Notifications import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities +import qs.modules.launcher as Launcher +import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts +import qs.modules.utilities as Utilities +import qs.modules.sidebar as Sidebar +import QtQuick +import QtQuick.Shapes Shape { id: root @@ -22,14 +23,14 @@ Shape { preferredRendererType: Shape.CurveRenderer Osd.Background { - wrapper: root.panels.osd // qmllint disable incompatible-type + wrapper: root.panels.osd startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Notifications.Background { - wrapper: root.panels.notifications // qmllint disable incompatible-type + wrapper: root.panels.notifications sidebar: sidebar startX: root.width @@ -37,28 +38,28 @@ Shape { } Session.Background { - wrapper: root.panels.session // qmllint disable incompatible-type + wrapper: root.panels.session startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Launcher.Background { - wrapper: root.panels.launcher // qmllint disable incompatible-type + wrapper: root.panels.launcher startX: (root.width - wrapper.width) / 2 - rounding startY: root.height } Dashboard.Background { - wrapper: root.panels.dashboard // qmllint disable incompatible-type + wrapper: root.panels.dashboard startX: (root.width - wrapper.width) / 2 - rounding startY: 0 } BarPopouts.Background { - wrapper: root.panels.popouts // qmllint disable incompatible-type + wrapper: root.panels.popouts invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height startX: wrapper.x @@ -66,7 +67,7 @@ Shape { } Utilities.Background { - wrapper: root.panels.utilities // qmllint disable incompatible-type + wrapper: root.panels.utilities sidebar: sidebar startX: root.width @@ -76,7 +77,7 @@ Shape { Sidebar.Background { id: sidebar - wrapper: root.panels.sidebar // qmllint disable incompatible-type + wrapper: root.panels.sidebar panels: root.panels startX: root.width diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 13f92a4b2..6fdd73bd7 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Effects Item { id: root diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c46..5b33076b8 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,17 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.components.containers import qs.services import qs.config import qs.utils import qs.modules.bar +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtQuick.Effects Variants { model: Screens.screens @@ -36,7 +35,7 @@ Variants { return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) + if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) return 0; const thresholds = []; @@ -55,16 +54,25 @@ Variants { screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + Region { + id: contextMenuRegion + x: panels.contextMenuX + bar.implicitWidth + y: panels.contextMenuY + Config.border.thickness + width: panels.contextMenuWidth + height: panels.contextMenuHeight + intersection: Intersection.Subtract + } mask: Region { - x: bar.clampedWidth + win.dragMaskPadding - y: Config.border.clampedThickness + win.dragMaskPadding - width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 - height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 + x: bar.implicitWidth + win.dragMaskPadding + y: Config.border.thickness + win.dragMaskPadding + width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 + height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor - regions: regions.instances // qmllint disable stale-property-read + regions: panels.contextMenuOpen ? regions.instances.concat([contextMenuRegion]) : regions.instances } anchors.top: true @@ -91,7 +99,7 @@ Variants { HyprlandFocusGrab { id: focusGrab - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + active: !panels.contextMenuOpen && ((visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1)) windows: [win] onCleared: { visibilities.launcher = false; @@ -133,9 +141,17 @@ Variants { } } - DrawerVisibilities { + PersistentProperties { id: visibilities + property bool bar + property bool osd + property bool session + property bool launcher + property bool dashboard + property bool utilities + property bool sidebar + Component.onCompleted: Visibilities.load(scope.modelData, this) } @@ -152,6 +168,7 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + windowRef: win } BarWrapper { diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index f43afb9a2..e4015c89a 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -1,16 +1,15 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components.containers import qs.config -import qs.modules.bar as Bar +import Quickshell +import QtQuick Scope { id: root required property ShellScreen screen - required property Bar.BarWrapper bar + required property Item bar ExclusionZone { anchors.left: true diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a93..9579b15ae 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,20 +1,17 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import qs.components import qs.components.controls import qs.config -import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts +import Quickshell +import QtQuick CustomMouseArea { id: root required property ShellScreen screen required property BarPopouts.Wrapper popouts - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property Panels panels - required property Bar.BarWrapper bar + required property Item bar property point dragStart property bool dashboardShortcutActive @@ -36,15 +33,15 @@ CustomMouseArea { } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); + return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); + return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); } - function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { - return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); + function inBottomPanel(panel: Item, x: real, y: real): bool { + return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { @@ -71,7 +68,7 @@ CustomMouseArea { if (!utilitiesShortcutActive) visibilities.utilities = false; - if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { + if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { popouts.hasCurrent = false; bar.closeTray(); } @@ -91,11 +88,11 @@ CustomMouseArea { const dragY = y - dragStart.y; // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) + if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) bar.isHovered = true; // Show/hide bar on drag - if (pressed && dragStart.x < bar.clampedWidth) { + if (pressed && dragStart.x < bar.implicitWidth) { if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) @@ -116,7 +113,7 @@ CustomMouseArea { root.panels.osd.hovered = true; } - const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); + const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; // Show/hide session on drag if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { @@ -191,7 +188,7 @@ CustomMouseArea { } // Show utilities on hover - const showUtilities = inBottomPanel(panels.utilities, x, y, true); + const showUtilities = inBottomPanel(panels.utilities, x, y); // Always update visibility based on hover if not in shortcut mode if (!utilitiesShortcutActive) { @@ -204,7 +201,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } @@ -212,6 +209,8 @@ CustomMouseArea { // Monitor individual visibility changes Connections { + target: root.visibilities + function onLauncherChanged() { // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { @@ -271,7 +270,5 @@ CustomMouseArea { root.utilitiesShortcutActive = false; } } - - target: root.visibilities } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa3..c99bd9029 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,24 +1,24 @@ -import QtQuick -import Quickshell -import qs.components import qs.config -import qs.modules.bar as Bar -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications import qs.modules.osd as Osd +import qs.modules.notifications as Notifications import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities +import qs.modules.launcher as Launcher +import qs.modules.launcher.items.contextmenu as LauncherItems +import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts +import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts +import qs.modules.sidebar as Sidebar +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities - required property Bar.BarWrapper bar + required property PersistentProperties visibilities + required property Item bar + required property var windowRef readonly property alias osd: osd readonly property alias notifications: notifications @@ -30,6 +30,12 @@ Item { readonly property alias toasts: toasts readonly property alias sidebar: sidebar + readonly property bool contextMenuOpen: launcherContextMenu.visible + readonly property real contextMenuX: launcherContextMenu.visible ? launcherContextMenu.mapToItem(root, launcherContextMenu.gooBounds.x, launcherContextMenu.gooBounds.y).x : 0 + readonly property real contextMenuY: launcherContextMenu.visible ? launcherContextMenu.mapToItem(root, launcherContextMenu.gooBounds.x, launcherContextMenu.gooBounds.y).y : 0 + readonly property real contextMenuWidth: launcherContextMenu.visible ? launcherContextMenu.gooBounds.width : 0 + readonly property real contextMenuHeight: launcherContextMenu.visible ? launcherContextMenu.gooBounds.height : 0 + anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth @@ -50,9 +56,7 @@ Item { id: notifications visibilities: root.visibilities - sidebarPanel: sidebar - osdPanel: osd - sessionPanel: session + panels: root anchors.top: parent.top anchors.right: parent.right @@ -79,6 +83,64 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom + + onRequestShowContextMenu: (app, clickX, clickY) => { + const panelsPos = launcher.mapToItem(root, clickX, clickY); + launcherContextMenu.showAt(app, panelsPos.x, panelsPos.y); + } + + onRequestCloseContextMenu: { + if (launcherContextMenu.visible) { + launcherContextMenu.toggle(); + } + } + } + + LauncherItems.ContextMenu { + id: launcherContextMenu + + property real menuX: -10000 + property real menuY: -10000 + + x: menuX + y: menuY + z: 10000 + visible: false + visibilities: root.visibilities + windowRef: root.windowRef + + onVisibleChanged: { + if (!visible) { + menuX = -10000; + menuY = -10000; + } + } + + function showAt(app: var, x: real, y: real): void { + if (launcherContextMenu.visible) { + launcherContextMenu.toggle(); + return; + } + + launcherContextMenu.app = app; + + const menuWidth = 250; + const menuHeight = Math.max(launcherContextMenu.implicitHeight || 300, 100); + const padding = 16; + const spacing = 4; + + const posX = Math.max(padding, Math.min(x, root.width - menuWidth - padding)); + const tooWide = menuWidth + padding * 2 > root.width; + + launcherContextMenu.menuX = tooWide ? padding : posX; + launcherContextMenu.width = tooWide ? root.width - padding * 2 : menuWidth; + + const spaceBelow = root.height - y; + launcherContextMenu.showAbove = spaceBelow < menuHeight + spacing; + launcherContextMenu.menuY = launcherContextMenu.showAbove ? Math.max(0, y - menuHeight - spacing) : y + spacing; + + launcherContextMenu.toggle(); + } } Dashboard.Wrapper { diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 15d6e44a6..176e25942 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -1,25 +1,138 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell +import "items" +import "services" import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config -import qs.modules.launcher.items -import qs.modules.launcher.services +import Quickshell +import QtQuick StyledListView { id: root required property StyledTextField search - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities + required property string activeCategory model: ScriptModel { id: model - onValuesChanged: root.currentIndex = 0 + onValuesChanged: { + root.currentIndex = 0; + } + } + + // Force model refresh when favourites change + Connections { + target: Config.launcher + function onFavouriteAppsChanged() { + if (root.state === "apps") { + model.values = root.filterAppsByCategory(Apps.search(search.text)); + } + } + } + + property string previousCategory: "" + property var pendingModelUpdate: null + + onActiveCategoryChanged: { + if (previousCategory !== "" && root.state === "apps") { + if (categoryChangeAnimation.running) { + categoryChangeAnimation.stop(); + root.opacity = 1; + root.scale = 1; + } + pendingModelUpdate = root.filterAppsByCategory(Apps.search(search.text)); + categoryChangeAnimation.start(); + } + previousCategory = activeCategory; + } + + SequentialAnimation { + id: categoryChangeAnimation + + ParallelAnimation { + Anim { + target: root + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + target: root + property: "scale" + to: 0.95 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasizedAccel + } + } + + ScriptAction { + script: { + // Update model while invisible + if (root.pendingModelUpdate !== null) { + model.values = root.pendingModelUpdate; + root.pendingModelUpdate = null; + } + } + } + + ParallelAnimation { + Anim { + target: root + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: root + property: "scale" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } + } + } + + function appHasCategory(appId: string, categoryName: string): bool { + if (!Config.launcher.categories) return false; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || category.name.toLowerCase() !== categoryName.toLowerCase()) continue; + if (!category.apps) continue; + + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + return true; + } + } + } + } + return false; + } + + function filterAppsByCategory(apps) { + if (root.activeCategory === "all") { + return apps; + } else if (root.activeCategory === "favourites") { + return apps.filter(app => { + const appId = app.id || app.entry?.id; + return Config.launcher.favouriteApps && Config.launcher.favouriteApps.includes(appId); + }); + } else { + // Custom category + return apps.filter(app => { + const appId = app.id || app.entry?.id; + return appHasCategory(appId, root.activeCategory); + }); + } } spacing: Appearance.spacing.small @@ -72,8 +185,10 @@ StyledListView { name: "apps" PropertyChanges { - model.values: Apps.search(search.text) + model.values: root.filterAppsByCategory(Apps.search(search.text)) root.delegate: appItem + root.opacity: 1 + root.scale: 1 } }, State { @@ -167,30 +282,58 @@ StyledListView { add: Transition { enabled: !root.state - Anim { - properties: "opacity,scale" - from: 0 - to: 1 + ParallelAnimation { + Anim { + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + property: "scale" + from: 0.8 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } } } remove: Transition { enabled: !root.state - Anim { - properties: "opacity,scale" - from: 1 - to: 0 + ParallelAnimation { + Anim { + property: "opacity" + from: 1 + to: 0 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + property: "scale" + from: 1 + to: 0.8 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasizedAccel + } } } move: Transition { - Anim { - property: "y" - } - Anim { - properties: "opacity,scale" - to: 1 + ParallelAnimation { + Anim { + property: "y" + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + Anim { + properties: "opacity,scale" + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } } @@ -215,11 +358,16 @@ StyledListView { } } + property var showContextMenuAt: null + property Item wrapperRoot: null + Component { id: appItem AppItem { visibilities: root.visibilities + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } } diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml index 508c75d4b..709c7d035 100644 --- a/modules/launcher/Background.qml +++ b/modules/launcher/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 88ff73e6d..53d77c642 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,46 +1,122 @@ pragma ComponentBehavior: Bound -import QtQuick +import "services" +import "../../components" as Components import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config -import qs.modules.launcher.services +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels required property real maxHeight + readonly property alias searchField: search readonly property int padding: Appearance.padding.large readonly property int rounding: Appearance.rounding.large - implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + property var showContextMenuAt: null + property Item wrapperRoot: null + + property string activeCategory: "all" + property bool showNavbar: (Config.launcher.enableCategories ?? true) && !search.text.startsWith(Config.launcher.actionPrefix) + + readonly property var categoryList: [ + { + id: "all", + name: qsTr("All"), + icon: "apps" + }, + { + id: "favourites", + name: qsTr("Favourites"), + icon: "favorite" + } + ].concat(Config.launcher.categories.map(cat => ({ + id: cat.name.toLowerCase(), + name: cat.name, + icon: cat.icon + }))) + + function navigateCategory(direction: int): void { + const currentIndex = categoryList.findIndex(cat => cat.id === activeCategory); + if (currentIndex === -1) + return; + + const newIndex = currentIndex + direction; + if (newIndex >= 0 && newIndex < categoryList.length) { + activeCategory = categoryList[newIndex].id; + if (categoryNavbar) { + categoryNavbar.scrollToActiveTab(); + } + } + } + + implicitWidth: list.width + padding * 2 + implicitHeight: searchWrapper.implicitHeight + list.implicitHeight + categoryNavbar.height + (showNavbar ? padding * 2 : 0) + padding * 2 + Appearance.spacing.normal - Item { - id: listWrapper + Components.CategoryNavbar { + id: categoryNavbar + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding + anchors.topMargin: root.padding + + categories: root.categoryList + activeCategory: root.activeCategory + showScrollButtons: true + + opacity: root.showNavbar ? 1 : 0 + height: root.showNavbar ? implicitHeight : 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on height { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + onCategoryChanged: categoryId => { + root.activeCategory = categoryId; + } + } - implicitWidth: list.width - implicitHeight: list.height + root.padding + ContentList { + id: list anchors.horizontalCenter: parent.horizontalCenter + anchors.top: categoryNavbar.bottom anchors.bottom: searchWrapper.top + anchors.topMargin: root.showNavbar ? root.padding : 0 anchors.bottomMargin: root.padding - ContentList { - id: list - - content: root - visibilities: root.visibilities - panels: root.panels - maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3 - search: search - padding: root.padding - rounding: root.rounding - } + content: root + visibilities: root.visibilities + panels: root.panels + maxHeight: root.maxHeight - searchWrapper.implicitHeight - categoryNavbar.implicitHeight - (root.showNavbar ? root.padding * 2 : 0) - root.padding * 4 + search: search + padding: root.padding + rounding: root.rounding + activeCategory: root.activeCategory + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } StyledRect { @@ -80,6 +156,10 @@ Item { placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + onTextChanged: { + root.showNavbar = !text.startsWith(Config.launcher.actionPrefix); + } + onAccepted: { const currentItem = list.currentList?.currentItem; if (currentItem) { @@ -103,6 +183,20 @@ Item { Keys.onUpPressed: list.currentList?.decrementCurrentIndex() Keys.onDownPressed: list.currentList?.incrementCurrentIndex() + Keys.onLeftPressed: event => { + if (event.modifiers === Qt.NoModifier) { + root.navigateCategory(-1); + event.accepted = true; + } + } + + Keys.onRightPressed: event => { + if (event.modifiers === Qt.NoModifier) { + root.navigateCategory(1); + event.accepted = true; + } + } + Keys.onEscapePressed: root.visibilities.launcher = false Keys.onPressed: event => { @@ -129,6 +223,8 @@ Item { Component.onCompleted: forceActiveFocus() Connections { + target: root.visibilities + function onLauncherChanged(): void { if (!root.visibilities.launcher) search.text = ""; @@ -138,8 +234,6 @@ Item { if (!root.visibilities.session) search.forceActiveFocus(); } - - target: root.visibilities } } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 775b193fa..5d494cca6 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -1,27 +1,30 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Item { id: root required property var content - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels required property real maxHeight required property StyledTextField search required property int padding required property int rounding + required property string activeCategory readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) - readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly + readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top anchors.bottom: parent.bottom clip: true @@ -73,6 +76,9 @@ Item { } } + property var showContextMenuAt: null + property Item wrapperRoot: null + Loader { id: appList @@ -83,13 +89,15 @@ Item { sourceComponent: AppList { search: root.search visibilities: root.visibilities + activeCategory: root.activeCategory + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } } Loader { id: wallpaperList - asynchronous: true active: false anchors.top: parent.top diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index ad03e9dec..4aba4365b 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "items" -import QtQuick -import Quickshell import qs.components.controls import qs.services import qs.config +import Quickshell +import QtQuick PathView { id: root @@ -18,7 +18,7 @@ PathView { readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 readonly property int numItems: { - const screen = (QsWindow.window as QsWindow)?.screen; + const screen = QsWindow.window?.screen; if (!screen) return 0; @@ -58,7 +58,7 @@ PathView { onCurrentItemChanged: { if (currentItem) - Wallpapers.preview((currentItem as WallpaperItem).modelData.path); + Wallpapers.preview(currentItem.modelData.path); } implicitWidth: Math.min(numItems, count) * itemWidth diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index cc5e86c6a..322be7680 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -1,19 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell +import "items" import qs.components import qs.config +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled property int contentHeight + property bool animationComplete: false readonly property real maxHeight: { let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; @@ -32,9 +34,12 @@ Item { if (shouldBeActive) { timer.stop(); hideAnim.stop(); + root.animationComplete = false; showAnim.start(); } else { + root.requestCloseContextMenu(); showAnim.stop(); + root.animationComplete = false; hideAnim.start(); } } @@ -50,7 +55,13 @@ Item { easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + script: { + root.implicitHeight = Qt.binding(() => content.implicitHeight); + // Wait one more frame after animation to ensure layout is stable + Qt.callLater(() => { + root.animationComplete = true; + }); + } } } @@ -69,6 +80,8 @@ Item { } Connections { + target: Config.launcher + function onEnabledChanged(): void { timer.start(); } @@ -76,17 +89,15 @@ Item { function onMaxShownChanged(): void { timer.start(); } - - target: Config.launcher } Connections { + target: DesktopEntries.applications + function onValuesChanged(): void { if (DesktopEntries.applications.values.length < Config.launcher.maxShown) timer.start(); } - - target: DesktopEntries.applications } Timer { @@ -123,8 +134,36 @@ Item { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight + showContextMenuAt: root.showContextMenu + wrapperRoot: root + + Component.onCompleted: { + root.contentHeight = implicitHeight; + Qt.callLater(() => { + root.animationComplete = true; + }); + } + } + } + + signal requestShowContextMenu(app: DesktopEntry, clickX: real, clickY: real) + signal requestCloseContextMenu - Component.onCompleted: root.contentHeight = implicitHeight + function restoreFocus(): void { + if (content.item && content.item.searchField) { + content.item.searchField.forceActiveFocus(); } } + + function showContextMenu(app: DesktopEntry, clickX: real, clickY: real): void { + if (!app || !root.animationComplete) { + return; + } + + if (clickX < 0 || clickX > root.width || clickY < 0 || clickY > root.height) { + return; + } + + root.requestShowContextMenu(app, clickX, clickY); + } } diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index a3ef00dda..e15802907 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,7 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config +import QtQuick Item { id: root @@ -15,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 0b64c3da0..0f1d7ca6c 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,30 +1,51 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets +pragma NativeMethodBehavior: AcceptThisObject + +import "../services" import qs.components import qs.services import qs.config import qs.utils -import qs.modules.launcher.services +import Quickshell +import Quickshell.Widgets +import QtQuick Item { id: root required property DesktopEntry modelData - required property DrawerVisibilities visibilities - + required property PersistentProperties visibilities + property var showContextMenuAt: null + property Item wrapperRoot: null + implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - function onClicked(): void { - Apps.launch(root.modelData); - root.visibilities.launcher = false; - } - + id: stateLayer radius: Appearance.rounding.normal + acceptedButtons: Qt.LeftButton | Qt.RightButton + + function onClicked(event): void { + if (event.button === Qt.LeftButton) { + Apps.launch(root.modelData); + root.visibilities.launcher = false; + } else if (event.button === Qt.RightButton) { + if (!root.showContextMenuAt || !root.wrapperRoot || !root.modelData) { + return; + } + + try { + const pos = stateLayer.mapToItem(root.wrapperRoot, event.x, event.y); + if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') { + root.showContextMenuAt(root.modelData, pos.x, pos.y); + } + } catch (error) { + console.error("Failed to show context menu:", error); + } + } + } } Item { @@ -36,7 +57,6 @@ Item { IconImage { id: icon - asynchronous: true source: Quickshell.iconPath(root.modelData?.icon, "image-missing") implicitSize: parent.height * 0.8 @@ -75,10 +95,9 @@ Item { Loader { id: favouriteIcon - asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id) + active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) sourceComponent: MaterialIcon { text: "favorite" diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 369b35ef6..65489d9bc 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia import qs.components import qs.services import qs.config +import Caelestia +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -13,26 +13,21 @@ Item { readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) function onClicked(): void { - Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); + Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); root.list.visibilities.launcher = false; } - onMathChanged: { - if (math.length > 0) - Qalculator.evalAsync(math); - } - implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.onClicked(); } - - radius: Appearance.rounding.normal } RowLayout { @@ -60,7 +55,7 @@ Item { return Colours.palette.m3onSurface; } - text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") + text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") elide: Text.ElideLeft Layout.fillWidth: true @@ -80,12 +75,12 @@ Item { StateLayer { id: stateLayer + color: Colours.palette.m3onTertiary + function onClicked(): void { Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } - - color: Colours.palette.m3onTertiary } StyledText { diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 6cc47aa48..3ff184681 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,8 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config -import qs.modules.launcher.services +import QtQuick Item { id: root @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { @@ -89,7 +89,6 @@ Item { Loader { id: current - asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index b63539b73..5c34fa89f 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,8 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config -import qs.modules.launcher.services +import QtQuick Item { id: root @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { @@ -65,7 +65,6 @@ Item { Loader { id: current - asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index beb93426a..9fdac3f38 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,20 +1,21 @@ -import QtQuick -import Caelestia.Models import qs.components import qs.components.effects import qs.components.images import qs.services import qs.config +import Caelestia.Models +import Quickshell +import QtQuick Item { id: root required property FileSystemEntry modelData - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities scale: 0.5 opacity: 0 - z: PathView.z ?? 0 // qmllint disable missing-property + z: PathView.z ?? 0 Component.onCompleted: { scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); @@ -25,12 +26,12 @@ Item { implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } - - radius: Appearance.rounding.normal } Elevation { diff --git a/modules/launcher/items/contextmenu/ActionRegistry.qml b/modules/launcher/items/contextmenu/ActionRegistry.qml new file mode 100644 index 000000000..c1405e60a --- /dev/null +++ b/modules/launcher/items/contextmenu/ActionRegistry.qml @@ -0,0 +1,222 @@ +pragma Singleton +import qs.utils +import qs.config +import Quickshell +import QtQuick + +QtObject { + id: registry + + readonly property var presets: ({ + "launch": { + text: qsTr("Launch"), + icon: "play_arrow", + hasSubmenu: true, + bold: true, + execute: function (app, context) { + if (app) { + context.launchApp(); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + } + }, + "workspaces": { + text: qsTr("Open in Workspace"), + icon: "workspaces", + hasSubmenu: true + }, + "categories": { + text: qsTr("Assign Categories"), + icon: "category", + hasSubmenu: true + }, + "favorites": { + text: qsTr("Add to Favourites"), + icon: "favorite", + dynamicText: true, + execute: function (app, context) { + if (!app || !app.id) + return context.toggle(); + const favourites = Config.launcher.favouriteApps.slice(); + const index = favourites.indexOf(app.id); + if (index > -1) + favourites.splice(index, 1); + else + favourites.push(app.id); + Config.launcher.favouriteApps = favourites; + Config.save(); + context.toggle(); + } + }, + "hide": { + text: qsTr("Hide from Launcher"), + icon: "visibility_off", + execute: function (app, context) { + if (!app || !app.id) + return context.toggle(); + const hidden = Config.launcher.hiddenApps.slice(); + hidden.push(app.id); + Config.launcher.hiddenApps = hidden; + Config.save(); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + }, + "desktop-file": { + text: qsTr("Open .desktop File"), + icon: "description", + execute: function (app, context) { + if (!app || !app.id) + return; + Quickshell.execDetached({ + command: ["sh", "-c", `file=$(find ~/.local/share/applications /usr/share/applications /usr/local/share/applications /var/lib/flatpak/exports/share/applications ~/.local/share/flatpak/exports/share/applications -name '${app.id}.desktop' 2>/dev/null | head -n1); [ -n "$file" ] && xdg-open "$file"`] + }); + context.toggle(); + } + } + }) + + readonly property var actions: ({ + "terminal": { + text: qsTr("Run in Terminal"), + icon: "terminal", + execute: function (app, context) { + if (!app || !app.execString) + return; + Quickshell.execDetached({ + command: [...Config.general.apps.terminal, "-e", app.execString] + }); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + }, + "kill": { + text: qsTr("Force Quit"), + icon: "close", + execute: function (app, context) { + if (!app || !app.execString) + return; + const execName = app.execString.split(" ")[0].split("/").pop(); + Quickshell.execDetached({ + command: ["pkill", "-9", "-f", execName] + }); + context.toggle(); + } + }, + "open-path": { + text: qsTr("Open App Location"), + icon: "folder_open", + execute: function (app, context) { + if (!app || !app.execString) + return; + const execPath = app.execString.split(" ")[0]; + Quickshell.execDetached({ + command: ["sh", "-c", `realpath=$(which "${execPath}" 2>/dev/null || realpath "${execPath}" 2>/dev/null); [ -n "$realpath" ] && ${Config.general.apps.explorer.join(" ")} "$(dirname "$realpath")"`] + }); + context.toggle(); + } + }, + "copy-exec": { + text: qsTr("Copy Command"), + icon: "content_copy", + execute: function (app, context) { + if (!app || !app.execString) + return; + Quickshell.execDetached({ + command: ["sh", "-c", `echo -n "${app.execString}" | wl-copy`] + }); + context.toggle(); + } + } + }) + + readonly property string defaultSubmenuIcon: "folder" + + function isPreset(id) { + return presets.hasOwnProperty(id); + } + + function isAction(id) { + return actions.hasOwnProperty(id); + } + + function isKnown(id) { + return id === "separator" || isPreset(id) || isAction(id); + } + + function getDefaults(id) { + if (id === "separator") { + return { + type: "separator" + }; + } + if (presets.hasOwnProperty(id)) { + return Object.assign({ + type: "preset" + }, presets[id]); + } + if (actions.hasOwnProperty(id)) { + return Object.assign({ + type: "action" + }, actions[id]); + } + return { + type: "custom", + text: id, + icon: defaultSubmenuIcon + }; + } + + function getText(id, config, app) { + if (id === "favorites" && app) { + const isFavorite = app.id && Strings.testRegexList(Config.launcher.favouriteApps, app.id); + return isFavorite ? qsTr("Remove from Favourites") : qsTr("Add to Favourites"); + } + + if (config && config.text) { + return config.text; + } + + const defaults = getDefaults(id); + return defaults.text || id; + } + + function getIcon(id, config) { + if (config && config.icon) { + return config.icon; + } + + const defaults = getDefaults(id); + return defaults.icon || defaultSubmenuIcon; + } + + function execute(actionId, config, app, context) { + const defaults = getDefaults(actionId); + + if (defaults.execute) { + defaults.execute(app, context); + } else if (config && config.command) { + executeCustomCommand(config.command, app, context); + } + } + + function executeCustomCommand(command, app, context) { + if (!Array.isArray(command)) + return; + + const processedCommand = command.map(arg => { + if (typeof arg !== "string") + return arg; + return arg.replace(/\$appId/g, app?.id || "").replace(/\$appName/g, app?.name || "").replace(/\$execString/g, app?.execString || "").replace(/\$desktopPath/g, app?.desktopPath || "").replace(/\$iconName/g, app?.iconName || ""); + }); + + Quickshell.execDetached({ + command: processedCommand + }); + context.toggle(); + } +} diff --git a/modules/launcher/items/contextmenu/ContextMenu.qml b/modules/launcher/items/contextmenu/ContextMenu.qml new file mode 100644 index 000000000..abbfb65df --- /dev/null +++ b/modules/launcher/items/contextmenu/ContextMenu.qml @@ -0,0 +1,640 @@ +import "../../services" +import "../../../../services" as Services +import "." as ContextMenus +import qs.components +import qs.services +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + property DesktopEntry app: null + property PersistentProperties visibilities + property var windowRef: null + property bool showAbove: false + readonly property alias gooBounds: gooBounds + property int activeSubmenuIndex: -1 + property int targetSubmenuIndex: -1 + property real submenuProgress: 0 + property int hoveredSubmenuIndex: -1 + property bool transitionContentVisible: false + property real contentOpacity: { + if (targetSubmenuIndex >= 0) { + return transitionContentVisible ? 1 : 0; + } + if (activeSubmenuIndex >= 0) { + return submenuProgress; + } + return 0; + } + property real submenuItemY: 0 + property int displayedSubmenuIndex: -1 + property real targetWidth: 0 + property real targetHeight: 0 + property real previousTargetWidth: 0 + property real previousTargetHeight: 0 + property real previousTopY: 0 + property real gooOverlapPx: 28 + readonly property real gooMarginPx: 30 + readonly property real bottomPadding: 16 + property string currentPage: "main" + + onVisibleChanged: { + if (!visible) + currentPage = "main"; + else if (app) + buildSubmenuMap(); + } + + function navigateToPage(page) { + if (currentPage === page) + return; + menuColumn.opacity = 0; + pageTransitionTimer.page = page; + pageTransitionTimer.restart(); + } + + Timer { + id: pageTransitionTimer + interval: 75 + property string page: "main" + + onTriggered: { + currentPage = page; + Qt.callLater(() => { + menuColumn.opacity = 1; + }); + } + } + + readonly property var menuConfigMain: (Config.launcher.contextMenuMain && Config.launcher.contextMenuMain.length > 0) ? Config.launcher.contextMenuMain : [ + { + "launch": { + "text": "Launch", + "icon": "play_arrow", + "bold": true + } + }, + "separator", "favorites", "categories", "hide", "workspaces"] + + readonly property var menuConfigAdvanced: (Config.launcher.contextMenuAdvanced && Config.launcher.contextMenuAdvanced.length > 0) ? Config.launcher.contextMenuAdvanced : ["terminal", + { + "desktop-file": { + "text": "edit .desktop File", + "icon": "code" + } + }, + "open-path", "separator", + { + "custom-submenu": { + "text": "Advanced Options", + "icon": "settings" + } + }, + { + "kill": { + "parent": "custom-submenu" + } + }, + { + "separator": { + "parent": "custom-submenu" + } + }, + { + "copy-exec": { + "parent": "custom-submenu" + } + } + ] + + readonly property var menuConfig: ({ + "main": menuConfigMain, + "advanced": menuConfigAdvanced + }) + readonly property bool hasAdvancedItems: menuConfig.advanced && menuConfig.advanced.length > 0 + + property var menuContext: ({ + visibilities: root.visibilities, + toggle: root.toggle, + launchApp: root.launchApp, + navigateToPage: root.navigateToPage + }) + + ContextMenus.MenuItemFactory { + id: menuFactory + app: root.app + visibilities: root.visibilities + launchApp: root.launchApp + toggle: root.toggle + menuContext: root.menuContext + } + + readonly property var processedMainItems: menuFactory.processMenuItems(menuConfig.main || []) + readonly property var processedAdvancedItems: menuFactory.processMenuItems(menuConfig.advanced || []) + readonly property var currentPageItems: currentPage === "main" ? processedMainItems : processedAdvancedItems + + property var submenuMap: ({}) + property int nextSubmenuIndex: 0 + property int submenuMapVersion: 0 + + function updateSubmenuDimensions() { + targetWidth = submenuColumn.implicitWidth + Appearance.padding.smaller * 2; + targetHeight = submenuColumn.implicitHeight + Appearance.padding.smaller * 2; + } + + function buildSubmenuMap() { + submenuMap = {}; + nextSubmenuIndex = 0; + + const allItems = [...processedMainItems, ...processedAdvancedItems]; + allItems.forEach(item => { + const isSubmenu = menuFactory.shouldShowAsSubmenu(item.id, item.config, processedMainItems) || menuFactory.shouldShowAsSubmenu(item.id, item.config, processedAdvancedItems); + if (isSubmenu) { + submenuMap[item.id] = nextSubmenuIndex++; + } + }); + submenuMapVersion++; + } + + function getSubmenuIndex(itemId) { + submenuMapVersion; + return submenuMap[itemId] ?? -1; + } + + Component.onCompleted: buildSubmenuMap() + onProcessedMainItemsChanged: buildSubmenuMap() + onProcessedAdvancedItemsChanged: buildSubmenuMap() + onAppChanged: buildSubmenuMap() + + function getSubmenuItemsForIndex(index) { + const allItems = [...processedMainItems, ...processedAdvancedItems]; + for (const [itemId, submenuIdx] of Object.entries(submenuMap)) { + if (submenuIdx === index) { + const item = allItems.find(i => i.id === itemId); + return item ? item.children : []; + } + } + return []; + } + + function getSubmenuParentId(index) { + for (const [itemId, submenuIdx] of Object.entries(submenuMap)) { + if (submenuIdx === index) { + return itemId; + } + } + return ""; + } + + Timer { + id: contentSwitchTimer + interval: Appearance.anim.durations.small + onTriggered: { + if (targetSubmenuIndex >= 0) { + activeSubmenuIndex = displayedSubmenuIndex = targetSubmenuIndex; + targetSubmenuIndex = -1; + } + transitionContentVisible = true; + if (activeSubmenuIndex >= 0) + Qt.callLater(updateSubmenuDimensions); + } + } + + onHoveredSubmenuIndexChanged: { + if (hoveredSubmenuIndex < 0) + return; + if (activeSubmenuIndex < 0) { + activeSubmenuIndex = displayedSubmenuIndex = hoveredSubmenuIndex; + targetSubmenuIndex = -1; + Qt.callLater(updateSubmenuDimensions); + } else if (activeSubmenuIndex !== hoveredSubmenuIndex) { + previousTargetWidth = targetWidth; + previousTargetHeight = targetHeight; + previousTopY = submenuContainer.interpolatedTopY; + targetSubmenuIndex = hoveredSubmenuIndex; + transitionContentVisible = false; + contentSwitchTimer.restart(); + } + } + + onActiveSubmenuIndexChanged: { + if (activeSubmenuIndex < 0) { + targetSubmenuIndex = displayedSubmenuIndex = -1; + targetWidth = targetHeight = previousTargetWidth = previousTargetHeight = previousTopY = 0; + } else if (displayedSubmenuIndex < 0) { + displayedSubmenuIndex = activeSubmenuIndex; + Qt.callLater(updateSubmenuDimensions); + } + } + + Connections { + target: submenuColumn + function onImplicitWidthChanged() { + if (displayedSubmenuIndex >= 0) + updateSubmenuDimensions(); + } + function onImplicitHeightChanged() { + if (displayedSubmenuIndex >= 0) + updateSubmenuDimensions(); + } + } + + visible: false + property bool menuOpen: false + + function launchApp(workspace) { + if (!root.app) + return; + if (workspace) + Services.Hypr.dispatch(`workspace ${workspace}`); + Apps.launch(root.app); + root.visibilities.launcher = false; + toggle(); + } + + function toggle() { + if (!root.app) + return; + + menuOpen = !root.visible; + if (menuOpen) { + activeSubmenuIndex = -1; + submenuProgress = 0; + root.visible = true; + } + } + + HyprlandFocusGrab { + active: root.visible && root.windowRef !== null + windows: root.windowRef ? [root.windowRef] : [] + + onCleared: { + if (root.visible) { + toggle(); + } + } + } + + Behavior on submenuProgress { + NumberAnimation { + duration: Appearance.anim.durations.normal / 1.2 + easing.type: Easing.InOutCubic + } + } + + Behavior on contentOpacity { + enabled: targetSubmenuIndex >= 0 + Anim { + duration: Appearance.anim.durations.normal / 1.2 + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Timer { + id: submenuCloseTimer + interval: 150 + onTriggered: { + if (hoveredSubmenuIndex < 0 && targetSubmenuIndex < 0 && !submenuHover.hovered) { + submenuProgress = 0; + Qt.callLater(() => { + if (submenuProgress === 0) + activeSubmenuIndex = -1; + }); + } + } + } + + Item { + id: menuWrapper + width: menuContainer.width + height: menuContainer.height + + opacity: menuOpen ? 1 : 0 + scale: menuOpen ? 1 : 0.85 + transformOrigin: Item.TopLeft + + Behavior on scale { + NumberAnimation { + duration: Appearance.anim.durations.fast || 150 + easing.type: Easing.OutCubic + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.fast || 150 + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + onOpacityChanged: { + if (opacity === 0 && !menuOpen && root.visible) { + root.visible = false; + root.app = null; + activeSubmenuIndex = -1; + submenuProgress = 0; + } + } + + Item { + id: gooBounds + + readonly property real menuLeft: menuContainer.x + readonly property real menuTop: menuContainer.y + readonly property real menuRight: menuContainer.x + menuContainer.width + readonly property real menuBottom: menuContainer.y + menuContainer.height + + readonly property bool hasSub: submenuContainer.visible + readonly property real subLeft: submenuContainer.x - root.gooOverlapPx + readonly property real subTop: submenuContainer.y + readonly property real subRight: submenuContainer.x + submenuContainer.width + readonly property real subBottom: submenuContainer.y + submenuContainer.height + + readonly property real gooLeft: (hasSub ? Math.min(menuLeft, subLeft) : menuLeft) - root.gooMarginPx + readonly property real gooTop: (hasSub ? Math.min(menuTop, subTop) : menuTop) - root.gooMarginPx + readonly property real gooRight: (hasSub ? Math.max(menuRight, subRight) : menuRight) + root.gooMarginPx + readonly property real gooBottom: (hasSub ? Math.max(menuBottom, subBottom) : menuBottom) + root.gooMarginPx + + x: gooLeft + y: gooTop + width: Math.max(1, gooRight - gooLeft) + height: Math.max(1, gooBottom - gooTop) + } + + ShaderEffect { + id: gooEffect + x: gooBounds.x + y: gooBounds.y + width: gooBounds.width + height: gooBounds.height + z: -2 + visible: root.visible + + property vector2d sizePx: Qt.vector2d(width, height) + + property vector4d menuRectPx: Qt.vector4d(menuContainer.x - gooBounds.x, menuContainer.y - gooBounds.y, menuContainer.width, menuContainer.height) + + property vector4d subRectPx: submenuContainer.visible ? Qt.vector4d((submenuContainer.x - gooBounds.x) - 24, submenuContainer.y - gooBounds.y, submenuContainer.width + 24, submenuContainer.height) : Qt.vector4d(0, 0, 0, 0) + + property real radiusPx: Appearance.rounding.normal * 0.75 + + readonly property real topEdgeDiff: submenuContainer.visible ? Math.abs(menuContainer.y - submenuContainer.y) : 999 + readonly property real bottomEdgeDiff: submenuContainer.visible ? Math.abs((menuContainer.y + menuContainer.height) - (submenuContainer.y + submenuContainer.height)) : 999 + readonly property bool isTopAligned: submenuContainer.visible && topEdgeDiff < 3 + readonly property bool isBottomAligned: submenuContainer.visible && bottomEdgeDiff < 3 + + property real smoothPxTop: isTopAligned ? 0 : 12 + property real smoothPxBottom: isBottomAligned ? 0 : 12 + + property color fillColor: Colours.palette.m3surfaceContainer + property color shadowColor: Qt.rgba(0, 0, 0, 0.20) + property vector2d shadowOffsetPx: Qt.vector2d(0, 0) + property real shadowSoftPx: 6 + + vertexShader: Qt.resolvedUrl("shaders/goo_sdf.vert.qsb") + fragmentShader: Qt.resolvedUrl("shaders/goo_sdf.frag.qsb") + } + + Item { + id: menuContainer + width: menuColumn.implicitWidth + Appearance.padding.smaller * 2 + height: menuColumn.implicitHeight + Appearance.padding.smaller * 2 + + Behavior on width { + NumberAnimation { + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.InOutQuad + } + } + + Behavior on height { + NumberAnimation { + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.InOutQuad + } + } + + ColumnLayout { + id: menuColumn + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + clip: true + opacity: 1 + + Behavior on opacity { + NumberAnimation { + duration: (Appearance.anim.durations.fast || 150) / 2 + easing.type: Easing.InOutQuad + } + } + + // Back button for advanced page + MenuItem { + visible: currentPage === "advanced" + text: qsTr("Back") + icon: "arrow_back" + onTriggered: navigateToPage("main") + } + + MenuItem { + isSeparator: true + visible: currentPage === "advanced" + } + + Repeater { + model: currentPageItems + + MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : menuFactory.createMenuItem(modelData, false, -1) + readonly property int dynamicSubmenuIndex: modelData.id === "separator" ? -1 : getSubmenuIndex(modelData.id) + + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: dynamicSubmenuIndex >= 0 + submenuIndex: dynamicSubmenuIndex + isSubmenuItem: false + + onTriggered: { + if (itemData?.onTriggered) { + itemData.onTriggered(); + } + } + } + } + + // More Options button for main page + MenuItem { + isSeparator: true + visible: currentPage === "main" && hasAdvancedItems + } + + MenuItem { + visible: currentPage === "main" && hasAdvancedItems + text: qsTr("More Options") + icon: "more_horiz" + onTriggered: navigateToPage("advanced") + } + } + } + + Item { + id: submenuContainer + z: -1 + + readonly property bool isTransitioning: targetSubmenuIndex >= 0 + readonly property bool shouldShowSubmenu: activeSubmenuIndex >= 0 && submenuProgress > 0 + + property real interpolatedWidth: targetWidth + property real interpolatedHeight: targetHeight + property real interpolatedTopY: isTransitioning ? previousTopY : submenuItemY - targetHeight / 2 + + property real centerOffset: (interpolatedHeight - interpolatedHeight * submenuProgress) / 2 + property real clampedY: { + const unclampedY = interpolatedTopY + centerOffset; + if (activeSubmenuIndex < 0 || height === 0) + return unclampedY; + + // Clamp to menu top edge when close + const topDiff = Math.abs(unclampedY - menuContainer.y); + if (topDiff < 3) { + return menuContainer.y; + } + + // Clamp to menu bottom edge when close + const menuBottom = menuContainer.y + menuContainer.height; + const subBottom = unclampedY + height; + const bottomDiff = Math.abs(subBottom - menuBottom); + if (bottomDiff < 3) { + return menuBottom - height; + } + + // Clamp to screen bottom + const maxY = (root.parent ? root.parent.height - root.y : 1000) - height - bottomPadding; + return Math.min(unclampedY, maxY); + } + + readonly property real slideOffsetX: -10 * (1 - submenuProgress) + + Behavior on interpolatedWidth { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.2 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + Behavior on interpolatedHeight { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.8 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + Behavior on interpolatedTopY { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.5 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + width: shouldShowSubmenu ? interpolatedWidth * submenuProgress : 0 + height: shouldShowSubmenu ? interpolatedHeight * submenuProgress : 0 + + x: menuContainer.width + slideOffsetX + y: clampedY + visible: width > 0 || height > 0 + clip: true + + HoverHandler { + id: submenuHover + } + + ColumnLayout { + id: submenuColumn + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + opacity: contentOpacity + + Loader { + id: launchSubmenuLoader + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "launch" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + + property var factoryRef: menuFactory + property var childrenRef: getSubmenuItemsForIndex(displayedSubmenuIndex) + + sourceComponent: ContextMenus.Submenus.LaunchSubmenu { + app: root.app + visibilities: root.visibilities + launchApp: root.launchApp + toggle: root.toggle + children: launchSubmenuLoader.childrenRef + menuFactory: launchSubmenuLoader.factoryRef + } + } + + Loader { + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "workspaces" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + sourceComponent: ContextMenus.Submenus.WorkspaceSubmenu { + launchApp: root.launchApp + } + } + + Loader { + id: categorySubmenuLoader + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "categories" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + sourceComponent: ContextMenus.Submenus.CategorySubmenu { + app: root.app + Component.onCompleted: initializeState() + Component.onDestruction: saveChanges() + } + } + + // Generic submenu for custom items + Repeater { + model: { + if (displayedSubmenuIndex < 0) + return []; + const parentId = getSubmenuParentId(displayedSubmenuIndex); + if (parentId === "launch" || parentId === "workspaces" || parentId === "categories") + return []; + return getSubmenuItemsForIndex(displayedSubmenuIndex); + } + + MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : menuFactory.createMenuItem(modelData, true, displayedSubmenuIndex) + + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: false + submenuIndex: -1 + isSubmenuItem: true + + onTriggered: itemData?.onTriggered?.() + } + } + } + } + } +} diff --git a/modules/launcher/items/contextmenu/MenuItem.qml b/modules/launcher/items/contextmenu/MenuItem.qml new file mode 100644 index 000000000..a72c170d0 --- /dev/null +++ b/modules/launcher/items/contextmenu/MenuItem.qml @@ -0,0 +1,113 @@ +import qs.components +import qs.config +import qs.services +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: item + property string text: "" + property string icon: "" + property bool bold: false + property bool hasSubMenu: false + property int submenuIndex: -1 + property bool isSubmenuItem: false + property bool isSeparator: false + property bool preventSubmenuClose: false + signal triggered + signal hovered + + Layout.fillWidth: true + Layout.minimumWidth: isSeparator ? 0 : (itemRow.implicitWidth + Appearance.padding.small * 2) + implicitHeight: isSeparator ? 1 : (32 + Appearance.padding.small * 2) + radius: isSeparator ? 0 : Appearance.rounding.small + color: "transparent" + + Timer { + id: openTimer + interval: 250 + onTriggered: if (item.hasSubMenu && mouse.containsMouse) { + activeSubmenuIndex = item.submenuIndex; + Qt.callLater(() => { + submenuProgress = 1; + }); + } + } + + MouseArea { + id: mouse + anchors.fill: parent + enabled: !item.isSeparator + hoverEnabled: !item.isSeparator + cursorShape: item.hasSubMenu ? Qt.ArrowCursor : Qt.PointingHandCursor + onClicked: if (!item.hasSubMenu) + item.triggered() + onEntered: { + item.color = Qt.alpha(Colours.palette.m3primary, 0.15); + item.hovered(); + if (item.hasSubMenu) { + hoveredSubmenuIndex = item.submenuIndex; + submenuItemY = item.y + item.height / 2; + submenuCloseTimer.stop(); + openTimer.restart(); + } else if (item.isSubmenuItem) { + submenuCloseTimer.stop(); + } else { + hoveredSubmenuIndex = -1; + if (activeSubmenuIndex >= 0) { + submenuCloseTimer.restart(); + } + } + } + onExited: { + openTimer.stop(); + item.color = "transparent"; + if (activeSubmenuIndex >= 0 && !item.preventSubmenuClose) { + hoveredSubmenuIndex = -1; + submenuCloseTimer.restart(); + } + } + onPressed: if (!item.hasSubMenu) + item.color = Qt.alpha(Colours.palette.m3primary, 0.2) + onReleased: if (!item.hasSubMenu) + item.color = containsMouse ? Qt.alpha(Colours.palette.m3primary, 0.15) : "transparent" + } + + RowLayout { + id: itemRow + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + visible: !item.isSeparator + + MaterialIcon { + text: item.icon + visible: text.length > 0 + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + } + StyledText { + text: item.text + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + font.weight: item.bold ? Font.DemiBold : Font.Normal + } + Item { + Layout.fillWidth: true + } + MaterialIcon { + text: "chevron_right" + visible: item.hasSubMenu + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + } + } + + Rectangle { + anchors.centerIn: parent + width: parent.width - Appearance.padding.small * 2 + height: 1 + color: Colours.palette.m3outlineVariant + visible: item.isSeparator + } +} diff --git a/modules/launcher/items/contextmenu/MenuItemFactory.qml b/modules/launcher/items/contextmenu/MenuItemFactory.qml new file mode 100644 index 000000000..75b8ac731 --- /dev/null +++ b/modules/launcher/items/contextmenu/MenuItemFactory.qml @@ -0,0 +1,135 @@ +pragma ComponentBehavior: Bound +import "." as ContextMenus +import QtQuick + +QtObject { + id: factory + + required property var app + required property var visibilities + required property var launchApp + required property var toggle + required property var menuContext + + readonly property var registry: ContextMenus.ActionRegistry + + function parseConfigItem(item) { + if (typeof item === "string") { + return { + id: item, + config: {} + }; + } else if (typeof item === "object" && item !== null) { + const id = Object.keys(item)[0]; + return { + id: id, + config: item[id] || {} + }; + } + return { + id: "", + config: {} + }; + } + + function processMenuItems(configArray) { + if (!configArray || typeof configArray.length !== 'number' || !configArray.forEach) + return []; + + const items = []; + const submenus = {}; + + // First pass: collect all items and identify submenu children + configArray.forEach((item, index) => { + const parsed = parseConfigItem(item); + const parent = parsed.config.parent; + + if (parent) { + if (!submenus[parent]) + submenus[parent] = []; + submenus[parent].push({ + id: parsed.id, + config: parsed.config, + index: index + }); + } else { + items.push({ + id: parsed.id, + config: parsed.config, + index: index, + children: [] + }); + } + }); + + // Second pass: attach children to their parent submenus + items.forEach(item => { + if (submenus[item.id]) { + item.children = submenus[item.id]; + } + }); + + return items; + } + + function hasChildren(items, id) { + const item = items.find(i => i.id === id); + return item && item.children && item.children.length > 0; + } + + function getSubmenuItems(items, id) { + const item = items.find(i => i.id === id); + return item ? item.children : []; + } + + function shouldShowAsSubmenu(id, config, items) { + // Launch preset as submenu only if has children OR app has .desktop actions + if (id === "launch") { + const hasConfigChildren = hasChildren(items, id); + const hasDesktopActions = app && app.actions && app.actions.length > 0; + return Boolean(hasConfigChildren || hasDesktopActions); + } + + if (id === "workspaces" || id === "categories") + return true; + + if (hasChildren(items, id)) + return true; + + return false; + } + + function createMenuItem(itemData, isSubmenuItem, submenuIndex) { + const id = itemData.id; + const config = itemData.config; + + if (id === "separator") { + return { + type: "separator" + }; + } + + const defaults = registry.getDefaults(id); + const text = registry.getText(id, config, app); + const icon = registry.getIcon(id, config); + const hasSubmenu = submenuIndex >= 0; + const bold = config.bold !== undefined ? config.bold : (defaults.bold || false); + + return { + type: "menuitem", + id: id, + text: text, + icon: icon, + bold: bold, + hasSubmenu: hasSubmenu, + submenuIndex: submenuIndex, + isSubmenuItem: isSubmenuItem, + config: config, + onTriggered: function () { + if (!hasSubmenu) { + registry.execute(id, config, app, menuContext); + } + } + }; + } +} diff --git a/modules/launcher/items/contextmenu/Submenus.qml b/modules/launcher/items/contextmenu/Submenus.qml new file mode 100644 index 000000000..e887d64bd --- /dev/null +++ b/modules/launcher/items/contextmenu/Submenus.qml @@ -0,0 +1,197 @@ +import qs.config +import qs.services +import qs.services as Services +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: submenus + + component LaunchSubmenu: ColumnLayout { + id: launchSubmenu + + required property DesktopEntry app + required property PersistentProperties visibilities + required property var launchApp + required property var toggle + property var children: [] + property var menuFactory: null + + spacing: Appearance.spacing.smaller + + MenuItem { + text: qsTr("Launch") + icon: "play_arrow" + isSubmenuItem: true + onTriggered: launchSubmenu.launchApp() + } + + Repeater { + model: launchSubmenu.app ? launchSubmenu.app.actions : [] + delegate: MenuItem { + required property var modelData + text: modelData.name || "" + icon: "play_arrow" + isSubmenuItem: true + visible: text.length > 0 + onTriggered: { + if (modelData && modelData.execute) + modelData.execute(); + if (launchSubmenu.visibilities) + launchSubmenu.visibilities.launcher = false; + launchSubmenu.toggle(); + } + } + } + + MenuItem { + isSeparator: true + visible: launchSubmenu.children && launchSubmenu.children.length > 0 + } + + Repeater { + model: launchSubmenu.children || [] + + delegate: MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : (launchSubmenu.menuFactory ? launchSubmenu.menuFactory.createMenuItem(modelData, true, -1) : null) + + visible: modelData.id === "separator" || (itemData !== null && itemData.text && itemData.text.length > 0) + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: false + submenuIndex: -1 + isSubmenuItem: true + + onTriggered: { + if (itemData?.onTriggered) { + itemData.onTriggered(); + } + } + } + } + } + + component WorkspaceSubmenu: ColumnLayout { + id: workspaceSubmenu + + required property var launchApp + + spacing: Appearance.spacing.smaller + + Repeater { + model: Services.Hypr.workspaces + delegate: MenuItem { + required property var modelData + property bool isCurrent: modelData.id === Services.Hypr.activeWsId + text: modelData.name || qsTr("Workspace %1").arg(modelData.id) + icon: isCurrent ? "radio_button_checked" : "radio_button_unchecked" + bold: isCurrent + isSubmenuItem: true + onTriggered: workspaceSubmenu.launchApp(modelData.id) + } + } + + MenuItem { + isSeparator: true + } + + MenuItem { + text: qsTr("New Workspace") + icon: "add_circle" + isSubmenuItem: true + onTriggered: { + Services.Hypr.dispatch("workspace empty"); + workspaceSubmenu.launchApp(); + } + } + } + + component CategorySubmenu: ColumnLayout { + id: categorySubmenu + + required property DesktopEntry app + + spacing: Appearance.spacing.smaller + + property var pendingChanges: ({}) + property int updateCounter: 0 + + function initializeState() { + pendingChanges = {}; + updateCounter = 0; + } + + function saveChanges() { + if (!categorySubmenu.app || Object.keys(pendingChanges).length === 0) + return; + + const appId = categorySubmenu.app.id; + const newCategories = []; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + const newCategory = { + name: category.name, + icon: category.icon, + apps: category.apps ? [...category.apps] : [] + }; + + if (pendingChanges.hasOwnProperty(category.name)) { + const shouldBeIncluded = pendingChanges[category.name]; + const index = newCategory.apps.indexOf(appId); + + if (shouldBeIncluded && index < 0) { + newCategory.apps.push(appId); + } else if (!shouldBeIncluded && index >= 0) { + newCategory.apps.splice(index, 1); + } + } + + newCategories.push(newCategory); + } + + Config.launcher.categories = newCategories; + Config.save(); + pendingChanges = {}; + } + + Repeater { + model: Config.launcher.categories || [] + delegate: MenuItem { + required property var modelData + + property bool checked: { + categorySubmenu.updateCounter; // Depend on counter + + if (!categorySubmenu.app || !modelData.apps) + return false; + const appId = categorySubmenu.app.id; + + if (categorySubmenu.pendingChanges.hasOwnProperty(modelData.name)) { + return categorySubmenu.pendingChanges[modelData.name]; + } + + for (let i = 0; i < modelData.apps.length; i++) { + if (modelData.apps[i] === appId) + return true; + } + return false; + } + + text: modelData.name || "" + icon: checked ? "check_box" : "check_box_outline_blank" + isSubmenuItem: true + preventSubmenuClose: true + + onTriggered: { + categorySubmenu.pendingChanges[modelData.name] = !checked; + categorySubmenu.updateCounter++; + } + } + } + } +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.frag b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag new file mode 100644 index 000000000..f7b074403 --- /dev/null +++ b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag @@ -0,0 +1,105 @@ +#version 440 +layout(location = 0) in vec2 vUv; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 sizePx; + vec4 menuRectPx; + vec4 subRectPx; + float radiusPx; + float smoothPxTop; + float smoothPxBottom; + + vec4 fillColor; + vec4 shadowColor; + vec2 shadowOffsetPx; + float shadowSoftPx; +} ubuf; + +// distance to rounded rectangle (centered at origin) +float sdRoundRect(vec2 p, vec2 b, float r) { + // b = half-size + vec2 q = abs(p) - (b - vec2(r)); + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; +} + +// smooth union (goo) +float smin(float a, float b, float k) { + // k in pixels-ish + float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0); + return mix(b, a, h) - k*h*(1.0 - h); +} + +vec4 premul(vec4 c) { return vec4(c.rgb * c.a, c.a); } + +void main() { + // local pixel coords in this effect item + vec2 p = vUv * ubuf.sizePx; + + // menu rect SDF + vec2 mPos = ubuf.menuRectPx.xy; + vec2 mSize = ubuf.menuRectPx.zw; + vec2 mCenter = mPos + mSize * 0.5; + float dm = sdRoundRect(p - mCenter, mSize * 0.5, ubuf.radiusPx); + + // optional submenu rect SDF + float d = dm; + if (ubuf.subRectPx.z > 0.0 && ubuf.subRectPx.w > 0.0) { + vec2 sPos = ubuf.subRectPx.xy; + vec2 sSize = ubuf.subRectPx.zw; + vec2 sCenter = sPos + sSize * 0.5; + float ds = sdRoundRect(p - sCenter, sSize * 0.5, ubuf.radiusPx); + + // Interpolate smoothing based on Y position + // Calculate merge point Y (where the shapes meet) + float mergeY = (mCenter.y + sCenter.y) * 0.5; + + // Interpolate smoothing: top uses smoothPxTop, bottom uses smoothPxBottom + float t = clamp((p.y - mergeY) / max(1.0, abs(mCenter.y - sCenter.y)), 0.0, 1.0); + float smoothPx = mix(ubuf.smoothPxTop, ubuf.smoothPxBottom, t); + + // smooth union or hard union (straight line when smoothPx <= 0) + if (smoothPx > 0.0) { + d = smin(dm, ds, max(1.0, smoothPx)); + } else { + d = min(dm, ds); + } + } + + // Fill alpha: inside shape => 1, outside => 0 with soft edge + float edge = 1.0; + float fillA = 1.0 - smoothstep(0.0, edge, d); + + // Shadow: evaluate SDF with an offset, soften more + vec2 ps = p - ubuf.shadowOffsetPx; + float dmS = sdRoundRect(ps - mCenter, mSize * 0.5, ubuf.radiusPx); + float dS = dmS; + if (ubuf.subRectPx.z > 0.0 && ubuf.subRectPx.w > 0.0) { + vec2 sPos = ubuf.subRectPx.xy; + vec2 sSize = ubuf.subRectPx.zw; + vec2 sCenter = sPos + sSize * 0.5; + float dsS = sdRoundRect(ps - sCenter, sSize * 0.5, ubuf.radiusPx); + // Use same interpolated smoothing for shadow + float mergeY = (mCenter.y + sCenter.y) * 0.5; + float t = clamp((ps.y - mergeY) / max(1.0, abs(mCenter.y - sCenter.y)), 0.0, 1.0); + float smoothPx = mix(ubuf.smoothPxTop, ubuf.smoothPxBottom, t); + + if (smoothPx > 0.0) { + dS = smin(dmS, dsS, max(1.0, smoothPx)); + } else { + dS = min(dmS, dsS); + } + } + // Start shadow closer (-2px) but fade over longer distance (2x softness) + float sh = 1.0 - smoothstep(-2.0, ubuf.shadowSoftPx * 2.0, dS); + float shadowOnly = max(sh - fillA, 0.0); + + vec4 f = premul(ubuf.fillColor); + vec4 s = premul(ubuf.shadowColor); + + vec4 outC = f * fillA + s * shadowOnly; + fragColor = outC * ubuf.qt_Opacity; +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..4caad0180691dcdf0c1b471ad9ffbcadb8525e00 GIT binary patch literal 5379 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 literal 0 HcmV?d00001 diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.vert b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert new file mode 100644 index 000000000..608d46aa5 --- /dev/null +++ b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert @@ -0,0 +1,27 @@ +#version 440 +layout(location = 0) in vec4 qt_Vertex; +layout(location = 1) in vec2 qt_MultiTexCoord0; + +layout(location = 0) out vec2 vUv; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 sizePx; + vec4 menuRectPx; + vec4 subRectPx; + float radiusPx; + float smoothPxTop; + float smoothPxBottom; + + vec4 fillColor; + vec4 shadowColor; + vec2 shadowOffsetPx; + float shadowSoftPx; +} ubuf; + +void main() { + vUv = qt_MultiTexCoord0; + gl_Position = ubuf.qt_Matrix * qt_Vertex; +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb new file mode 100644 index 0000000000000000000000000000000000000000..e236b719e525245a2d7120a3c04d9aa10ba2ddaa GIT binary patch literal 1888 zcmV-m2cP%=04(Tuob6iOQxjJdza&855}|2e@S{>36+6`#9WsDSXBZ}i zWCPnKyK(m}1RQ33>AQXL*+(DzU-~!nzvxS+J?GxDyEps6h%58ZT1*0Y21NYBzyxrtikL!+`ai51=X*(@rae-ZbLNC zjXpx$R?vgqWSCeMSreElz{1%sDZTB*AUyh+1He7-wF=LoD0BC~AOK`7% z{GuP-qH2mdhCl=Q8D)8r#4-pDm*Ef$V04H9y=W$RF#@oL$P>=C1j7onqNz6lVZU$s(U1VrovBWcb z#S+f!TpRN;eAawM^zU_&fA2cF=|3W zewcT7H9vb5E1wOBHX#+9q}G8-8fC-8c`uU%avH9$kRNt?Y0hZfM{6M;LT6h{hFK57Al*?Ksh%E3{ssy-;XbqP4a=>*{~u%Kpe|04N-``5tt6bp(S z>3;(S^KPEbsh?tf3wC>HeFqSz_e-+n4BK&4MlLxsMi;)x@D`%w#rp&r=2=O)WurO(L;8~on@;g z==*cotZN!I%Why)G#5UAv|YA$4Rce<+J3w}mo0M$(1y!(NyCu2b#AQatp&@ntA!9k z1a(!nx!yu?mQmI4geGzncaW*g$4SA>&3dV5IR?jLn-?bK{Ug$@sFL@y*@VM~D>d0U z{EvV5!HzVc1be`v+wU_kLg@S+;C;maRrhw{(x}D!q4toOP8$6WdWd)Up&rsX{7w%! zOb>af^pHP;G`iYJqqj~Y%jHT68D?Vc$gPjoFH< zlvp9`tcsnHV3}}XV17@9k_ka7hTvCYzLdp>M>w7!*bb{X<-!+X^PcFdcJ@Y8^%3!g#DL=s0C=R6N6!Y@BP`qDpHg#S}-o zIJLo2Yg;ZpSXwPD*p}mz9+*v+Q{sj>z~q&mepy&tT)J0ii$+ALf}EC4#i&|zc}SP= zC_zhGAON$xs)S|VYZuVDdFfqyM#HU7_|8s+iZmx0LDYWPp&}vFAnKcX$-ud1N;FLK zDlDI1WDx%dTyi*-t7NytLV{FEzC9dmi5J1jxQYxEO-|>oZH5%=CpAv|<@`;j zadsN#(QKURM%yFH2WqH6bb`m00C4^D>9~h~Qu~A(bYOTpOi7@l(8n_@_^%8Y>49;> ztkm7AuKn21dAVM4c(rb9+{y-Y$7oc)$B+EhEqHfu@P`Gn+|V5;WL*vHVIQ+szqbk5VYEoJ$B$k~LaTQkr=uS(X}r?iS? z!qOD^aanW<0LUi@VMWe=JNG2wHLpF}m-bnHt{Y}jAzP8!iM&QiX0@!TVk}uGSSj7Y zhES=%BTw4wluD;m4pk~~-}EZ#`e#^BAtv#q4=D9}KMdJ3 String(a.name + a.flavour).localeCompare((b.name + b.flavour))); + schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 73b6e6705..aa926acd1 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.images import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -54,7 +54,6 @@ ColumnLayout { } Loader { - asynchronous: true Layout.leftMargin: Appearance.spacing.small Layout.alignment: Qt.AlignVCenter @@ -135,12 +134,12 @@ ColumnLayout { } StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + function onClicked(): void { parent.forceActiveFocus(); } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor } RowLayout { @@ -194,11 +193,11 @@ ColumnLayout { radius: Appearance.rounding.full StateLayer { + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.lock.pam.passwd.start(); } - - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -357,6 +356,8 @@ ColumnLayout { } Connections { + target: root.lock.pam + function onFlashMsg(): void { exitAnim.stop(); if (message.scale < 1) @@ -364,8 +365,6 @@ ColumnLayout { else flashAnim.restart(); } - - target: root.lock.pam } Anim { diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 4427099fb..a024ddc23 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts RowLayout { id: root diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index ab6de7b6c..e96b14315 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell.Services.UPower import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell.Services.UPower +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -162,7 +162,6 @@ ColumnLayout { } component WrappedLoader: Loader { - asynchronous: true visible: active } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 1286267a7..358093f39 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -20,6 +20,8 @@ Item { clip: true Connections { + target: root.pam + function onBufferChanged(): void { if (root.pam.buffer.length > root.buffer.length) { charList.bindImWidth(); @@ -30,8 +32,6 @@ Item { root.buffer = root.pam.buffer; } - - target: root.pam } StyledText { diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 5d70266a0..6fd5277fe 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound +import qs.components.misc import Quickshell import Quickshell.Io import Quickshell.Wayland -import qs.components.misc Scope { property alias lock: lock @@ -25,23 +25,21 @@ Scope { lock: lock } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "lock" description: "Lock the current session" onPressed: lock.locked = true } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() } IpcHandler { + target: "lock" + function lock(): void { lock.locked = true; } @@ -53,7 +51,5 @@ Scope { function isLocked(): bool { return lock.locked; } - - target: "lock" } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 46862f83f..279c55138 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell.Wayland import qs.components import qs.services import qs.config +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects WlSessionLockSurface { id: root @@ -18,11 +18,11 @@ WlSessionLockSurface { color: "transparent" Connections { + target: root.lock + function onUnlock(): void { unlockAnim.start(); } - - target: root.lock } SequentialAnimation { diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index cabe60b9a..b7e58bbcb 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -18,7 +18,7 @@ Item { Image { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type + source: Players.active?.trackArtUrl ?? "" asynchronous: true fillMode: Image.PreserveAspectCrop @@ -110,34 +110,34 @@ Item { spacing: Appearance.spacing.large PlayerControl { + icon: "skip_previous" + function onClicked(): void { if (Players.active?.canGoPrevious) Players.active.previous(); } - - icon: "skip_previous" } PlayerControl { - function onClicked(): void { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } - animate: true icon: active ? "pause" : "play_arrow" colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false + + function onClicked(): void { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } } PlayerControl { + icon: "skip_next" + function onClicked(): void { if (Players.active?.canGoNext) Players.active.next(); } - - icon: "skip_next" } } } @@ -171,11 +171,11 @@ Item { StateLayer { id: controlState + color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] + function onClicked(): void { control.onClicked(); } - - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] } MaterialIcon { diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 62439d0c3..cce86cdf3 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -39,7 +39,6 @@ ColumnLayout { color: "transparent" Loader { - asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 133dc6266..7fcb108ec 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -109,14 +109,12 @@ StyledRect { radius: Appearance.rounding.full Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image @@ -176,11 +174,11 @@ StyledRect { Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface + function onClicked(): void { root.expanded = !root.expanded; } - - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { @@ -272,7 +270,6 @@ StyledRect { } Loader { - asynchronous: true Layout.fillWidth: true opacity: root.expanded ? 1 : 0 @@ -306,7 +303,7 @@ StyledRect { component NotifLine: StyledText { id: notifLine - required property NotifData modelData + required property Notifs.Notif modelData Layout.fillWidth: true textFormat: Text.MarkdownText diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 8345ebc4d..0186c2f84 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -1,9 +1,9 @@ -import QtQuick +import qs.config import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam -import qs.config +import QtQuick Scope { id: root @@ -132,7 +132,7 @@ Scope { id: availProc command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { // qmllint disable signal-handler-parameters + onExited: code => { fprint.available = code === 0; fprint.checkAvail(); } @@ -166,6 +166,8 @@ Scope { } Connections { + target: root.lock + function onSecureChanged(): void { if (root.lock.secure) { availProc.running = true; @@ -179,15 +181,13 @@ Scope { function onUnlock(): void { fprint.abort(); } - - target: root.lock } Connections { + target: Config.lock + function onEnableFprintChanged(): void { fprint.checkAvail(); } - - target: Config.lock } } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 33c0e4c00..82c004c22 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.misc import qs.services import qs.config +import QtQuick +import QtQuick.Layouts GridLayout { id: root diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index 213bf155d..d6c25af29 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,10 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import qs.utils +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -18,7 +19,6 @@ ColumnLayout { spacing: Appearance.spacing.small Loader { - asynchronous: true Layout.topMargin: Appearance.padding.large * 2 Layout.bottomMargin: -Appearance.padding.large Layout.alignment: Qt.AlignHCenter @@ -71,7 +71,6 @@ ColumnLayout { } Loader { - asynchronous: true Layout.rightMargin: Appearance.padding.smaller active: root.width > 400 visible: active @@ -108,7 +107,6 @@ ColumnLayout { Loader { id: forecastLoader - asynchronous: true Layout.topMargin: Appearance.spacing.smaller Layout.bottomMargin: Appearance.padding.large * 2 Layout.fillWidth: true diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 4d7a5ff76..a44cb19b7 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index bd18276ef..2d4590e0e 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,18 +1,16 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets -import qs.components import qs.components.containers import qs.components.widgets import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - required property Item osdPanel - required property Item sessionPanel + required property PersistentProperties visibilities + required property Item panels readonly property int padding: Appearance.padding.large anchors.top: parent.top @@ -27,21 +25,23 @@ Item { let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) - height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; + height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - if (visibilities.osd) { - const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities && panels) { + if (visibilities.osd) { + const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } - if (visibilities.session) { - const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; + if (visibilities.session) { + const h = panels.session.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } } - return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -62,9 +62,79 @@ Item { orientation: Qt.Vertical spacing: 0 - cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 + cacheBuffer: QsWindow.window?.screen.height ?? 0 + + delegate: Item { + id: wrapper + + required property Notifs.Notif modelData + required property int index + readonly property alias nonAnimHeight: notif.nonAnimHeight + property int idx + + onIndexChanged: { + if (index !== -1) + idx = index; + } + + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) + + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: true + } + PropertyAction { + target: wrapper + property: "enabled" + value: false + } + PropertyAction { + target: wrapper + property: "implicitHeight" + value: 0 + } + PropertyAction { + target: wrapper + property: "z" + value: 1 + } + Anim { + target: notif + property: "x" + to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: false + } + } + + ClippingRectangle { + anchors.top: parent.top + anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller + + color: "transparent" + radius: notif.radius + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight - delegate: NotifWrapper {} + Notification { + id: notif + + modelData: wrapper.modelData + } + } + } move: Transition { Anim { @@ -89,7 +159,7 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return i; @@ -110,7 +180,7 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return count - i - 1; @@ -126,78 +196,6 @@ Item { Anim {} } - component NotifWrapper: Item { - id: wrapper - - required property NotifData modelData - required property int index - readonly property alias nonAnimHeight: notif.nonAnimHeight - property int idx - - onIndexChanged: { - if (index !== -1) - idx = index; - } - - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) - - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: true - } - PropertyAction { - target: wrapper - property: "enabled" - value: false - } - PropertyAction { - target: wrapper - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: wrapper - property: "z" - value: 1 - } - Anim { - target: notif - property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: false - } - } - - ClippingRectangle { - anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller - - color: "transparent" - radius: notif.radius - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight - - Notification { - id: notif - - modelData: wrapper.modelData - } - } - } - component Anim: NumberAnimation { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 7b580a74f..c8efa8d78 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes StyledRect { id: root - required property NotifData modelData + required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText @@ -85,7 +85,7 @@ StyledRect { return; const actions = root.modelData.actions; - if (actions.length === 1) + if (actions?.length === 1) actions[0].invoke(); } @@ -109,7 +109,6 @@ StyledRect { Loader { id: image - asynchronous: true active: root.hasImage anchors.left: parent.left @@ -138,7 +137,6 @@ StyledRect { Loader { id: appIcon - asynchronous: true active: root.hasAppIcon || !root.hasImage anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter @@ -155,7 +153,6 @@ StyledRect { Loader { id: icon - asynchronous: true active: root.hasAppIcon anchors.centerIn: parent @@ -172,7 +169,6 @@ StyledRect { } Loader { - asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 @@ -361,12 +357,12 @@ StyledRect { implicitHeight: expandIcon.height StateLayer { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked() { root.expanded = !root.expanded; } - - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -460,7 +456,6 @@ StyledRect { Action { modelData: QtObject { readonly property string text: qsTr("Close") - function invoke(): void { root.modelData.close(); } @@ -492,12 +487,12 @@ StyledRect { implicitHeight: actionText.height + Appearance.padding.small * 2 StateLayer { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface + function onClicked(): void { action.modelData.invoke(); } - - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface } StyledText { diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 2b581e93e..61acc56e1 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,17 +1,15 @@ -import QtQuick import qs.components import qs.config +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - required property Item sidebarPanel - property alias osdPanel: content.osdPanel - property alias sessionPanel: content.sessionPanel + required property var visibilities + required property Item panels visible: height > 0 - implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) + implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { @@ -36,5 +34,6 @@ Item { id: content visibilities: root.visibilities + panels: root.panels } } diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index a609f4601..78955c7a8 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 5ec6ad983..770fb6968 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts Item { id: root required property Brightness.Monitor monitor - required property DrawerVisibilities visibilities + required property var visibilities required property real volume required property bool muted @@ -31,6 +31,9 @@ Item { // Speaker volume CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementVolume(); @@ -38,9 +41,6 @@ Item { Audio.decrementVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -56,6 +56,9 @@ Item { shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) sourceComponent: CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementSourceVolume(); @@ -63,9 +66,6 @@ Item { Audio.decrementSourceVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -82,6 +82,9 @@ Item { shouldBeActive: Config.osd.enableBrightness sourceComponent: CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { const monitor = root.monitor; if (!monitor) @@ -92,9 +95,6 @@ Item { monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -109,7 +109,6 @@ Item { component WrappedLoader: Loader { required property bool shouldBeActive - asynchronous: true Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 939b57de1..2519609dd 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property var visibilities property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) @@ -71,6 +71,8 @@ Item { ] Connections { + target: Audio + function onMutedChanged(): void { root.show(); root.muted = Audio.muted; @@ -90,17 +92,15 @@ Item { root.show(); root.sourceVolume = Audio.sourceVolume; } - - target: Audio } Connections { + target: root.monitor + function onBrightnessChanged(): void { root.show(); root.brightness = root.monitor?.brightness ?? 0; } - - target: root.monitor } Timer { diff --git a/modules/session/Background.qml b/modules/session/Background.qml index a609f4601..78955c7a8 100644 --- a/modules/session/Background.qml +++ b/modules/session/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 405fce1ce..45152e28a 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Column { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities padding: Appearance.padding.large spacing: Appearance.spacing.large @@ -26,12 +26,12 @@ Column { Component.onCompleted: forceActiveFocus() Connections { + target: root.visibilities + function onLauncherChanged(): void { if (!root.visibilities.launcher) logout.forceActiveFocus(); } - - target: root.visibilities } } @@ -115,12 +115,12 @@ Column { } StateLayer { + radius: parent.radius + color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked(): void { Quickshell.execDetached(button.command); } - - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 2924f776f..14b03a809 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -1,13 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.config +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels readonly property real nonAnimWidth: content.implicitWidth diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml index 4cc142628..beefdf5c4 100644 --- a/modules/sidebar/Background.qml +++ b/modules/sidebar/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index 7dbbd06c3..1b7feed66 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -1,14 +1,14 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root required property Props props - required property DrawerVisibilities visibilities + required property var visibilities ColumnLayout { id: layout diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index fe56798f7..5a317640f 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts StyledRect { id: root - required property NotifData modelData + required property Notifs.Notif modelData required property Props props required property bool expanded - required property DrawerVisibilities visibilities + required property var visibilities - readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null + readonly property StyledText body: expandedContent.item?.body ?? null readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight @@ -118,45 +118,42 @@ StyledRect { anchors.right: parent.right anchors.topMargin: Appearance.spacing.small / 2 - sourceComponent: ExpandedBody {} - } + sourceComponent: ColumnLayout { + readonly property alias body: body - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - component ExpandedBody: ColumnLayout { - readonly property alias body: bodyText + spacing: Appearance.spacing.smaller - spacing: Appearance.spacing.smaller + StyledText { + id: body - StyledText { - id: bodyText + Layout.fillWidth: true + textFormat: Text.MarkdownText + text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + wrapMode: Text.WordWrap - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline - wrapMode: Text.WordWrap + onLinkActivated: link => { + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.visibilities.sidebar = false; + } + } - onLinkActivated: link => { - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.visibilities.sidebar = false; + NotifActionList { + notif: root.modelData } } + } - NotifActionList { - notif: root.modelData + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component WrappedLoader: Loader { required property bool shouldBeActive - asynchronous: true opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 370a79cbc..d1f1e1f51 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root - required property NotifData notif + required property Notifs.Notif notif Layout.fillWidth: true implicitHeight: flickable.contentHeight @@ -167,7 +167,6 @@ Item { id: iconComp IconImage { - asynchronous: true source: Quickshell.iconPath(action.modelData.identifier) } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index f041cc9be..d039d15d6 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root required property Props props - required property DrawerVisibilities visibilities + required property var visibilities readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent @@ -86,7 +86,6 @@ Item { color: "transparent" Loader { - asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 @@ -96,7 +95,7 @@ Item { Image { asynchronous: true - source: Quickshell.shellPath("assets/dino.png") + source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 @@ -156,19 +155,17 @@ Item { let next = null; for (let i = 0; i < notifList.repeater.count; i++) { next = notifList.repeater.itemAt(i); - if (!next?.closed) // qmllint disable missing-property + if (!next?.closed) break; } - if (next) { - next.closeAll(); // qmllint disable missing-property - } else { + if (next) + next.closeAll(); + else stop(); - } } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.normal diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 7ffb64182..b927e91a7 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root required property Props props required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property alias repeater: repeater readonly property int spacing: Appearance.spacing.small @@ -39,130 +39,128 @@ Item { onValuesChanged: root.flagChanged() } - delegate: NotifGroupDelegate {} - } - - component NotifGroupDelegate: MouseArea { - id: notif + MouseArea { + id: notif - required property int index - required property string modelData + required property int index + required property string modelData - readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY + readonly property bool closed: notifInner.notifCount === 0 + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY - function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } + function closeAll(): void { + for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) + n.close(); + } - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifGroupDelegate; - if (item && !item.closed) - y += item.nonAnimHeight + root.spacing; + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.closed) + y += item.nonAnimHeight + root.spacing; + } + return y; } - return y; - } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } } - } - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: true - enabled: !closed + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: true + enabled: !closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - notifInner.toggleExpand(!notifInner.expanded); - else if (event.button === Qt.MiddleButton) - closeAll(); - } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - notifInner.toggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + notifInner.toggleExpand(!notifInner.expanded); + else if (event.button === Qt.MiddleButton) + closeAll(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - closeAll(); - } - - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + notifInner.toggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + closeAll(); } - } - - ParallelAnimation { - running: notif.closed - Anim { - target: notif - property: "opacity" - to: 0 + ParallelAnimation { + running: true + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - Anim { - target: notif - property: "scale" - to: 0.6 + + ParallelAnimation { + running: notif.closed + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "scale" + to: 0.6 + } } - } - NotifGroup { - id: notifInner + NotifGroup { + id: notifInner - modelData: notif.modelData - props: root.props - container: root.container - visibilities: root.visibilities - } + modelData: notif.modelData + props: root.props + container: root.container + visibilities: root.visibilities + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index f1d1502f9..2c032aa7d 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -16,7 +16,7 @@ StyledRect { required property string modelData required property Props props required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) readonly property var groupProps: { @@ -136,14 +136,12 @@ StyledRect { radius: Appearance.rounding.full Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image @@ -204,11 +202,11 @@ StyledRect { radius: Appearance.rounding.full StateLayer { + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + function onClicked(): void { root.toggleExpand(!root.expanded); } - - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fe7510b55..e586b5f7a 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -14,13 +14,13 @@ Item { required property list notifs required property bool expanded required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property real nonAnimHeight: { let h = -root.spacing; for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) h += item.nonAnimHeight + root.spacing; } return h; @@ -59,157 +59,155 @@ Item { onValuesChanged: root.flagChanged() } - delegate: NotifDelegate {} - } + MouseArea { + id: notif - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - component NotifDelegate: MouseArea { - id: notif - - required property int index - required property NotifData modelData + required property int index + required property Notifs.Notif modelData - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + readonly property bool previewHidden: { + if (root.expanded) + return false; - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i].closed) + extraHidden++; - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; + return index >= Config.notifs.groupPreviewNum + extraHidden; + } + property int startY + + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) + y += item.nonAnimHeight + root.spacing; + } + return y; } - return y; - } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } } - } - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 + opacity: previewHidden ? 0 : 1 + scale: previewHidden ? 0.7 : 1 - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded + enabled: !modelData.closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData.close(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData.close(); } - } - - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) - Anim { - target: notif - property: "opacity" - to: 0 + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) + + ParallelAnimation { + Component.onCompleted: running = !notif.previewHidden + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0.7 + to: 1 + } } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width + + ParallelAnimation { + running: notif.modelData.closed + onFinished: notif.modelData.unlock(notif) + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width + } } - } - Notif { - id: notifInner + Notif { + id: notifInner - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities - } + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + visibilities: root.visibilities + } - Behavior on opacity { - Anim {} - } + Behavior on opacity { + Anim {} + } - Behavior on scale { - Anim {} - } + Behavior on scale { + Anim {} + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index ad2564131..9303c6b94 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.config +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property var visibilities required property var panels readonly property Props props: Props {} diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index 975461a58..fbce89616 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 6dcfedcdf..902656de5 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,16 +1,14 @@ import "cards" +import qs.config import QtQuick import QtQuick.Layouts -import qs.components -import qs.config -import qs.modules.bar.popouts as BarPopouts Item { id: root required property var props - required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts + required property var visibilities + required property Item popouts implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 4d6d8af2d..127afe93b 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -1,21 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Caelestia import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Caelestia +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes Loader { id: root required property var props - asynchronous: true anchors.fill: parent opacity: root.props.recordingConfirmDelete ? 1 : 0 diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 66a616f07..77178e36e 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -1,17 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property var visibilities required property Item sidebar - required property BarPopouts.Wrapper popouts + required property Item popouts readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -80,7 +79,6 @@ Item { Loader { id: content - asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 20d232a37..0344e3ad2 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -1,9 +1,9 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -70,7 +70,6 @@ StyledRect { Loader { id: activeChip - asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left anchors.topMargin: Appearance.spacing.larger diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 9dd16ec51..273c64002 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root required property var props - required property DrawerVisibilities visibilities + required property var visibilities Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 @@ -111,7 +111,6 @@ StyledRect { property bool running: Recorder.running - asynchronous: true Layout.fillWidth: true Layout.preferredHeight: implicitHeight sourceComponent: running ? recordingControls : recordingList diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 7fb56f3ba..b9d757a4d 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -1,22 +1,23 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Models import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config import qs.utils +import Caelestia +import Caelestia.Models +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root required property var props - required property DrawerVisibilities visibilities + required property var visibilities spacing: 0 @@ -162,7 +163,6 @@ ColumnLayout { } Loader { - asynchronous: true anchors.centerIn: parent opacity: list.count === 0 ? 1 : 0 diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 996b027c3..d610586bf 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -1,19 +1,18 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config -import qs.modules.bar.popouts as BarPopouts +import qs.modules.controlcenter +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts StyledRect { id: root - required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts + required property var visibilities + required property Item popouts readonly property var quickToggles: { const seenIds = new Set(); @@ -21,13 +20,15 @@ StyledRect { return Config.utilities.quickToggles.filter(item => { if (!item.enabled) return false; - + if (seenIds.has(item.id)) { return false; } if (item.id === "vpn") { - return Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); + return Config.utilities.vpn.provider.some(p => + typeof p === "object" ? (p.enabled === true) : false + ); } seenIds.add(item.id); @@ -55,17 +56,17 @@ StyledRect { font.pointSize: Appearance.font.size.normal } - QuickToggleRow { + ToggleRow { rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } - QuickToggleRow { + ToggleRow { visible: root.needExtraRow rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } - component QuickToggleRow: RowLayout { + component ToggleRow: RowLayout { property var rowModel: [] Layout.fillWidth: true @@ -89,9 +90,9 @@ StyledRect { roleValue: "bluetooth" delegate: Toggle { icon: "bluetooth" - checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.enabled ?? false onClicked: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = !adapter.enabled; } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 52473a48e..f47550006 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Caelestia import qs.components import qs.components.effects import qs.services import qs.config +import Caelestia +import QtQuick +import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index ac8772f57..2915404e0 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Caelestia import qs.components import qs.config +import Caelestia +import Quickshell +import QtQuick Item { id: root diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 15f71956c..89acfe6d6 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -1,11 +1,9 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.services import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -37,11 +35,11 @@ ColumnLayout { implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small StateLayer { + color: Colours.palette.m3onPrimary + function onClicked(): void { root.moveToWsExpanded = !root.moveToWsExpanded; } - - color: Colours.palette.m3onPrimary } MaterialIcon { @@ -83,14 +81,14 @@ ColumnLayout { readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId - function onClicked(): void { - Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); - } - color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer text: wsId disabled: isCurrent + + function onClicked(): void { + Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); + } } } } @@ -109,41 +107,40 @@ ColumnLayout { spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small Button { - function onClicked(): void { - Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") + + function onClicked(): void { + Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); + } } Loader { - asynchronous: true active: root.client?.lastIpcObject.floating Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing sourceComponent: Button { - function onClicked(): void { - Hypr.dispatch(`pin address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") + + function onClicked(): void { + Hypr.dispatch(`pin address:0x${root.client?.address}`); + } } } Button { - function onClicked(): void { - Hypr.dispatch(`killwindow address:0x${root.client?.address}`); - } - color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Kill") + + function onClicked(): void { + Hypr.dispatch(`killwindow address:0x${root.client?.address}`); + } } } @@ -163,11 +160,11 @@ ColumnLayout { StateLayer { id: stateLayer + color: parent.onColor + function onClicked(): void { parent.onClicked(); } - - color: parent.onColor } StyledText { diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 3934a993d..f9ee66a68 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -1,9 +1,9 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell.Hyprland import qs.components import qs.services import qs.config +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 2683e5447..4cc0aab86 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root @@ -33,7 +33,6 @@ Item { radius: Appearance.rounding.small Loader { - asynchronous: true anchors.centerIn: parent active: !root.client @@ -69,7 +68,7 @@ Item { anchors.centerIn: parent - captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type + captureSource: root.client?.wayland ?? null live: true constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index d7cfc2fa1..919b3fbb1 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root From 71d76e21cb5c9874dd0cbc1f8310af6c222023f0 Mon Sep 17 00:00:00 2001 From: AleksElixir <71710534+AleksElixir@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:55:26 +0200 Subject: [PATCH 4/4] Add files via upload --- README.md | 57 +- .../Internal/cachingimagemanager.cpp | 17 +- .../Internal/cachingimagemanager.hpp | 6 +- .../src/Caelestia/Internal/sparklineitem.cpp | 3 - .../src/Caelestia/Models/filesystemmodel.cpp | 40 +- .../src/Caelestia/Models/filesystemmodel.hpp | 2 +- .../src/Caelestia/Services/audiocollector.cpp | 2 +- .../src/Caelestia/Services/audioprovider.hpp | 2 +- plugin/src/Caelestia/Services/beattracker.cpp | 4 +- plugin/src/Caelestia/appdb.cpp | 12 +- plugin/src/Caelestia/cutils.cpp | 13 +- plugin/src/Caelestia/imageanalyser.cpp | 13 +- plugin/src/Caelestia/imageanalyser.hpp | 3 +- plugin/src/Caelestia/qalculator.cpp | 98 +-- plugin/src/Caelestia/qalculator.hpp | 23 - plugin/src/Caelestia/requests.cpp | 22 +- plugin/src/Caelestia/requests.hpp | 4 +- services/Audio.qml | 14 +- services/Brightness.qml | 28 +- services/Colours.qml | 9 +- services/GameMode.qml | 18 +- services/Hypr.qml | 24 +- services/IdleInhibitor.qml | 4 +- services/Network.qml | 111 ++-- services/NetworkUsage.qml | 10 +- services/Nmcli.qml | 566 +++++++++--------- services/Notifs.qml | 237 +++++++- services/Players.qml | 26 +- services/Recorder.qml | 8 +- services/Screens.qml | 2 +- services/SystemUsage.qml | 5 +- services/Time.qml | 5 +- services/VPN.qml | 10 +- services/Visibilities.qml | 6 +- services/Wallpapers.qml | 13 +- services/Weather.qml | 9 +- utils/Icons.qml | 4 +- utils/NetworkConnection.qml | 2 +- utils/Paths.qml | 5 +- utils/Searcher.qml | 3 +- utils/SysInfo.qml | 10 +- 41 files changed, 771 insertions(+), 679 deletions(-) diff --git a/README.md b/README.md index 0c96b29fa..8efc5cb9a 100644 --- a/README.md +++ b/README.md @@ -337,8 +337,6 @@ default, you must create it manually. "showOnHover": true }, "clock": { - "background": false, - "showDate": false, "showIcon": true }, "dragThreshold": 20, @@ -577,7 +575,57 @@ default, you must create it manually. }, "showOnHover": false, "favouriteApps": [], - "hiddenApps": [] + "hiddenApps": [], + "categories": [ + { + "name": "Development", + "icon": "code", + "apps": ["code-oss"] + }, + { + "name": "Graphics", + "icon": "palette", + "apps": [] + }, + { + "name": "Communication", + "icon": "chat", + "apps": ["vesktop", "discord"] + }, + { + "name": "Media", + "icon": "play_circle", + "apps": ["spotify", "obs"] + }, + { + "name": "Games", + "icon": "sports_esports", + "apps": ["steam"] + }, + { + "name": "Utilities", + "icon": "build", + "apps": ["obs"] + } + ], + "contextMenuMain": [ + {"launch": {"text": "Launch", "icon": "play_arrow", "bold": true}}, + {"terminal": {"parent": "launch"}}, + "separator", + "favorites", + "categories", + "hide", + "workspaces" + ], + "contextMenuAdvanced": [ + "open-path", + "desktop-file", + "separator", + {"custom-submenu": {"text": "Advanced Options", "icon": "settings"}}, + {"kill": {"parent": "custom-submenu"}}, + {"separator": {"parent": "custom-submenu"}}, + {"copy-exec": {"parent": "custom-submenu"}} + ] }, "lock": { "recolourLogo": false, @@ -600,8 +648,7 @@ default, you must create it manually. "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", - "wallpaperDir": "~/Pictures/Wallpapers", - "lyricsDir": "~/Music/lyrics" + "wallpaperDir": "~/Pictures/Wallpapers" }, "services": { "audioIncrement": 0.1, diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 9f63f99a2..1c15cd203 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -105,26 +105,34 @@ void CachingImageManager::updateSource(const QString& path) { m_shaPath = path; - QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) { + const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); + + const auto watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { if (m_path != path) { + // Object is destroyed or path has changed, ignore + watcher->deleteLater(); return; } const QSize size = effectiveSize(); if (!m_item || !size.width() || !size.height()) { + watcher->deleteLater(); return; } const QString fillMode = m_item->property("fillMode").toString(); // clang-format off const QString filename = QString("%1@%2x%3-%4.png") - .arg(sha).arg(size.width()).arg(size.height()) + .arg(watcher->result()).arg(size.width()).arg(size.height()) .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); // clang-format on const QUrl cache = m_cacheDir.resolved(QUrl(filename)); if (m_cachePath == cache) { + watcher->deleteLater(); return; } @@ -133,6 +141,7 @@ void CachingImageManager::updateSource(const QString& path) { if (!cache.isLocalFile()) { qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; + watcher->deleteLater(); return; } @@ -148,7 +157,11 @@ void CachingImageManager::updateSource(const QString& path) { if (m_shaPath == path) { m_shaPath = QString(); } + + watcher->deleteLater(); }); + + watcher->setFuture(future); } QUrl CachingImageManager::cachePath() const { diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 1b707414d..3611699b6 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -2,7 +2,6 @@ #include #include -#include #include namespace caelestia::internal { @@ -19,7 +18,8 @@ class CachingImageManager : public QObject { public: explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) {} + : QObject(parent) + , m_item(nullptr) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); @@ -46,7 +46,7 @@ class CachingImageManager : public QObject { private: QString m_shaPath; - QPointer m_item; + QQuickItem* m_item; QUrl m_cacheDir; QString m_path; diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp index 5ffcc2f84..b4938d1d2 100644 --- a/plugin/src/Caelestia/Internal/sparklineitem.cpp +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -27,9 +27,6 @@ void SparklineItem::paint(QPainter* painter) { } void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { - if (m_historyLength < 2) - return; - const qreal w = width(); const qreal h = height(); const int len = buffer->count(); diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp index 267a43946..4eb94cd49 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.cpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { if (m_recursive && m_watchChanges) { const auto currentDir = m_dir; const bool showHidden = m_showHidden; - auto future = QtConcurrent::run([showHidden, path]() { + const auto future = QtConcurrent::run([showHidden, path]() { QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; if (showHidden) { filters |= QDir::Hidden; @@ -232,12 +232,16 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { } return dirs; }); - future.then(this, [currentDir, showHidden, this](const QStringList& paths) { + const auto watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { + const auto paths = watcher->result(); if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { // Ignore if dir or showHidden has changed m_watcher.addPaths(paths); } + watcher->deleteLater(); }); + watcher->setFuture(future); } } @@ -291,7 +295,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { oldPaths << entry->path(); } - auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { + const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; std::optional iter; @@ -349,7 +353,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { newPaths.insert(path); } - if (promise.isCanceled()) { + if (promise.isCanceled() || newPaths == oldPaths) { return; } @@ -361,17 +365,23 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { } m_futures.insert(dir, future); - future - .then(this, - [dir, this](QPair, QSet> result) { - m_futures.remove(dir); - if (!result.first.isEmpty() || !result.second.isEmpty()) { - applyChanges(result.first, result.second); - } - }) - .onCanceled(this, [dir, this]() { - m_futures.remove(dir); - }); + const auto watcher = new QFutureWatcher, QSet>>(this); + + connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { + m_futures.remove(dir); + + if (!watcher->future().isResultReadyAt(0)) { + watcher->deleteLater(); + return; + } + + const auto result = watcher->result(); + applyChanges(result.first, result.second); + + watcher->deleteLater(); + }); + + watcher->setFuture(future); } void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp index c3315858a..cf8eae822 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.hpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp @@ -132,7 +132,7 @@ class FileSystemModel : public QAbstractListModel { bool m_recursive; bool m_watchChanges; bool m_showHidden; - bool m_sortReverse = false; + bool m_sortReverse; Filter m_filter; QStringList m_nameFilters; diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index fb051ccbd..15634059e 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -221,7 +221,7 @@ AudioCollector::AudioCollector(QObject* parent) , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { - AudioCollector::stop(); + stop(); } void AudioCollector::start() { diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp index 4b85929f4..5bf9bb00d 100644 --- a/plugin/src/Caelestia/Services/audioprovider.hpp +++ b/plugin/src/Caelestia/Services/audioprovider.hpp @@ -23,7 +23,7 @@ public slots: virtual void process() = 0; private: - QTimer* m_timer = nullptr; + QTimer* m_timer; }; class AudioProvider : public Service { diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp index 649705799..93addc679 100644 --- a/plugin/src/Caelestia/Services/beattracker.cpp +++ b/plugin/src/Caelestia/Services/beattracker.cpp @@ -19,9 +19,7 @@ BeatProcessor::~BeatProcessor() { if (m_in) { del_fvec(m_in); } - if (m_out) { - del_fvec(m_out); - } + del_fvec(m_out); } void BeatProcessor::process() { diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 1e33990da..6952c0e08 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -11,7 +11,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) , m_entry(entry) , m_frequency(frequency) { const auto mo = m_entry->metaObject(); - const auto tmo = &AppEntry::staticMetaObject; + const auto tmo = metaObject(); for (const auto& prop : { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { @@ -303,13 +303,11 @@ void AppDb::updateApps() { newIds.insert(entry->property("id").toString()); } - for (auto it = m_apps.begin(); it != m_apps.end();) { - if (!newIds.contains(it.key())) { + for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { + const auto& id = *it; + if (!newIds.contains(id)) { dirty = true; - it.value()->deleteLater(); - it = m_apps.erase(it); - } else { - ++it; + m_apps.take(id)->deleteLater(); } } diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index b6ec33a88..6e3bfa99c 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -75,20 +75,13 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { - QJSValueList args = { QJSValue(path.toLocalFile()) }; - if (engine) { - args << engine->toScriptValue(QVariant::fromValue(path)); - } - onSaved.call(args); + onSaved.call( + { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); } } else { qWarning() << "CUtils::saveItem: failed to save" << path; if (onFailed.isCallable()) { - if (engine) { - onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); - } else { - onFailed.call(); - } + onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); } } watcher->deleteLater(); diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 0b623162b..880b0785c 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -134,11 +134,6 @@ void ImageAnalyser::update() { if (m_sourceItem) { const QSharedPointer grabResult = m_sourceItem->grabToImage(); - if (!grabResult) { - QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, - Qt::SingleShotConnection); - return; - } QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); }); @@ -196,14 +191,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag continue; } - const quint32 mr = static_cast(pixel[2] & 0xF8); + const quint32 mr = static_cast(pixel[0] & 0xF8); const quint32 mg = static_cast(pixel[1] & 0xF8); - const quint32 mb = static_cast(pixel[0] & 0xF8); + const quint32 mb = static_cast(pixel[2] & 0xF8); ++colours[(mr << 16) | (mg << 8) | mb]; - const qreal r = pixel[2] / 255.0; + const qreal r = pixel[0] / 255.0; const qreal g = pixel[1] / 255.0; - const qreal b = pixel[0] / 255.0; + const qreal b = pixel[2] / 255.0; totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); ++count; } diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp index 63fbf9691..bbea2b32e 100644 --- a/plugin/src/Caelestia/imageanalyser.hpp +++ b/plugin/src/Caelestia/imageanalyser.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include namespace caelestia { @@ -49,7 +48,7 @@ class ImageAnalyser : public QObject { QFutureWatcher* const m_futureWatcher; QString m_source; - QPointer m_sourceItem; + QQuickItem* m_sourceItem; int m_rescaleSize; QColor m_dominantColour; diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp index c72421793..44e8d21e2 100644 --- a/plugin/src/Caelestia/qalculator.cpp +++ b/plugin/src/Caelestia/qalculator.cpp @@ -1,19 +1,13 @@ #include "qalculator.hpp" #include -#include namespace caelestia { -QMutex Qalculator::s_calculatorMutex; - Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { - // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), - // but we need to assign it to a var so compiler doesn't flag it as a leak - static const auto* const instance = new Calculator(); - Q_UNUSED(instance) + new Calculator(); CALCULATOR->loadExchangeRates(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); @@ -25,8 +19,6 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString(); } - QMutexLocker locker(&s_calculatorMutex); - EvaluationOptions eo; PrintOptions po; @@ -57,92 +49,4 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString::fromStdString(result); } -void Qalculator::evalAsync(const QString& expr) { - const quint64 gen = ++m_generation; - - if (expr.isEmpty()) { - if (!m_result.isEmpty()) { - m_result.clear(); - emit resultChanged(); - } - if (!m_rawResult.isEmpty()) { - m_rawResult.clear(); - emit rawResultChanged(); - } - if (m_busy) { - m_busy = false; - emit busyChanged(); - } - return; - } - - if (!m_busy) { - m_busy = true; - emit busyChanged(); - } - - QtConcurrent::run([expr]() -> QPair { - QMutexLocker locker(&s_calculatorMutex); - - EvaluationOptions eo; - PrintOptions po; - - std::string parsed; - std::string result = CALCULATOR->calculateAndPrint( - CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); - - std::string error; - while (CALCULATOR->message()) { - if (!CALCULATOR->message()->message().empty()) { - if (CALCULATOR->message()->type() == MESSAGE_ERROR) { - error += "error: "; - } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { - error += "warning: "; - } - error += CALCULATOR->message()->message(); - } - CALCULATOR->nextMessage(); - } - - if (!error.empty()) { - const QString errorStr = QString::fromStdString(error); - return { errorStr, errorStr }; - } - - const QString rawStr = QString::fromStdString(result); - return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; - }).then(this, [this, gen](QPair result) { - if (gen != m_generation) { - return; - } - - const auto& [formatted, raw] = result; - - if (m_result != formatted) { - m_result = formatted; - emit resultChanged(); - } - if (m_rawResult != raw) { - m_rawResult = raw; - emit rawResultChanged(); - } - if (m_busy) { - m_busy = false; - emit busyChanged(); - } - }); -} - -QString Qalculator::result() const { - return m_result; -} - -QString Qalculator::rawResult() const { - return m_rawResult; -} - -bool Qalculator::busy() const { - return m_busy; -} - } // namespace caelestia diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp index b2f5517f7..a07a8a2fc 100644 --- a/plugin/src/Caelestia/qalculator.hpp +++ b/plugin/src/Caelestia/qalculator.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -11,32 +10,10 @@ class Qalculator : public QObject { QML_ELEMENT QML_SINGLETON - Q_PROPERTY(QString result READ result NOTIFY resultChanged) - Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) - Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) - public: explicit Qalculator(QObject* parent = nullptr); Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; - Q_INVOKABLE void evalAsync(const QString& expr); - - [[nodiscard]] QString result() const; - [[nodiscard]] QString rawResult() const; - [[nodiscard]] bool busy() const; - -signals: - void resultChanged(); - void rawResultChanged(); - void busyChanged(); - -private: - static QMutex s_calculatorMutex; - - QString m_result; - QString m_rawResult; - bool m_busy = false; - quint64 m_generation = 0; }; } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index dbc746ed1..2ceddb351 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -1,8 +1,6 @@ #include "requests.hpp" -#include #include -#include #include #include @@ -12,27 +10,13 @@ Requests::Requests(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) {} -void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { +void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { if (!onSuccess.isCallable()) { qWarning() << "Requests::get: onSuccess is not callable"; return; } QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); - request.setRawHeader("Cache-Control", "no-cache, no-store"); - request.setRawHeader("Pragma", "no-cache"); - request.setRawHeader("Connection", "close"); - - if (headers.isObject()) { - QJSValueIterator it(headers); - while (it.hasNext()) { - it.next(); - request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); - } - } - auto reply = m_manager->get(request); QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { @@ -48,8 +32,4 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSVal }); } -void Requests::resetCookies() const { - m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); -} - } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp index d07d7e8f0..1db2f4cf6 100644 --- a/plugin/src/Caelestia/requests.hpp +++ b/plugin/src/Caelestia/requests.hpp @@ -14,9 +14,7 @@ class Requests : public QObject { public: explicit Requests(QObject* parent = nullptr); - Q_INVOKABLE void get( - const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; - Q_INVOKABLE void resetCookies() const; + Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; private: QNetworkAccessManager* m_manager; diff --git a/services/Audio.qml b/services/Audio.qml index 100f00101..14d0a4e81 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -1,11 +1,11 @@ pragma Singleton -import QtQuick +import qs.config +import Caelestia.Services +import Caelestia import Quickshell import Quickshell.Services.Pipewire -import Caelestia -import Caelestia.Services -import qs.config +import QtQuick Singleton { id: root @@ -92,7 +92,7 @@ Singleton { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name - return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); + return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { @@ -125,6 +125,8 @@ Singleton { } Connections { + target: Pipewire.nodes + function onValuesChanged(): void { const newSinks = []; const newSources = []; @@ -145,8 +147,6 @@ Singleton { root.sources = newSources; root.streams = newStreams; } - - target: Pipewire.nodes } PwObjectTracker { diff --git a/services/Brightness.qml b/services/Brightness.qml index 6e828d918..567824042 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -1,11 +1,11 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick +import qs.config +import qs.components.misc import Quickshell import Quickshell.Io -import qs.components.misc -import qs.config +import QtQuick Singleton { id: root @@ -17,34 +17,34 @@ Singleton { map[m.connector] = m; return map; } - readonly property list monitors: variants.instances // qmllint disable incompatible-type + readonly property list monitors: variants.instances property bool appleDisplayPresent: false function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); // qmllint disable missing-property + return monitors.find(m => m.modelData === screen); } function getMonitor(query: string): var { if (query === "active") { - return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); } if (query.startsWith("model:")) { const model = query.slice(6); - return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property + return monitors.find(m => m.modelData.model === model); } if (query.startsWith("serial:")) { const serial = query.slice(7); - return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property + return monitors.find(m => m.modelData.serialNumber === serial); } if (query.startsWith("id:")) { const id = parseInt(query.slice(3), 10); - return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); } - return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property + return monitors.find(m => m.modelData.name === query); } function increaseBrightness(): void { @@ -92,23 +92,21 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "brightnessUp" description: "Increase brightness" onPressed: root.increaseBrightness() } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "brightnessDown" description: "Decrease brightness" onPressed: root.decreaseBrightness() } IpcHandler { + target: "brightness" + function get(): real { return getFor("active"); } @@ -157,8 +155,6 @@ Singleton { return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; } - - target: "brightness" } component Monitor: QtObject { diff --git a/services/Colours.qml b/services/Colours.qml index 469118167..cd86c8fbf 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -1,13 +1,12 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root diff --git a/services/GameMode.qml b/services/GameMode.qml index 3a3fb7bda..83770b79f 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -1,11 +1,11 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia import qs.services import qs.config +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root @@ -40,21 +40,23 @@ Singleton { PersistentProperties { id: props - property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property + property bool enabled: Hypr.options["animations:enabled"] === 0 reloadableId: "gameMode" } Connections { + target: Hypr + function onConfigReloaded(): void { if (props.enabled) root.setDynamicConfs(); } - - target: Hypr } IpcHandler { + target: "gameMode" + function isEnabled(): bool { return props.enabled; } @@ -70,7 +72,5 @@ Singleton { function disable(): void { props.enabled = false; } - - target: "gameMode" } } diff --git a/services/Hypr.qml b/services/Hypr.qml index 98c6e7186..c703f7047 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -1,13 +1,13 @@ pragma Singleton -import QtQuick +import qs.components.misc +import qs.config +import Caelestia +import Caelestia.Internal import Quickshell import Quickshell.Hyprland import Quickshell.Io -import Caelestia -import Caelestia.Internal -import qs.components.misc -import qs.config +import QtQuick Singleton { id: root @@ -120,6 +120,8 @@ Singleton { } Connections { + target: Hyprland + function onRawEvent(event: HyprlandEvent): void { const n = event.name; if (n.endsWith("v2")) @@ -142,11 +144,11 @@ Singleton { Hyprland.refreshToplevels(); } } - - target: Hyprland } Connections { + target: root.focusedMonitor + function onLastIpcObjectChanged(): void { const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; @@ -154,8 +156,6 @@ Singleton { root.lastSpecialWorkspace = specialName; } } - - target: root.focusedMonitor } FileView { @@ -192,6 +192,8 @@ Singleton { } IpcHandler { + target: "hypr" + function refreshDevices(): void { extras.refreshDevices(); } @@ -203,13 +205,9 @@ Singleton { function listSpecialWorkspaces(): string { return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); } - - target: "hypr" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "refreshDevices" description: "Reload devices" onPressed: extras.refreshDevices() diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml index 9f556b3a4..29409abc1 100644 --- a/services/IdleInhibitor.qml +++ b/services/IdleInhibitor.qml @@ -35,6 +35,8 @@ Singleton { } IpcHandler { + target: "idleInhibitor" + function isEnabled(): bool { return props.enabled; } @@ -50,7 +52,5 @@ Singleton { function disable(): void { props.enabled = false; } - - target: "idleInhibitor" } } diff --git a/services/Network.qml b/services/Network.qml index 4e0b809ca..ede37c802 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -1,28 +1,44 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io +import QtQuick import qs.services Singleton { id: root + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: Nmcli.scanning + property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null - property var pendingConnection: null - property list savedConnections: [] - property list savedConnectionSsids: [] - - signal connectionFailed(string ssid) function enableWifi(enabled: bool): void { Nmcli.enableWifi(enabled, result => { @@ -50,6 +66,9 @@ Singleton { Nmcli.rescanWifi(); } + property var pendingConnection: null + signal connectionFailed(string ssid) + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { // Set up pending connection tracking if callback provided if (callback) { @@ -140,6 +159,20 @@ Singleton { }); } + property list savedConnections: [] + property list savedConnectionSsids: [] + + // Sync saved connections from Nmcli when they're updated + Connections { + target: Nmcli + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + } + function syncNetworksFromNmcli(): void { const rNetworks = root.networks; const nNetworks = Nmcli.networks; @@ -184,6 +217,22 @@ Singleton { } } + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + function hasSavedProfile(ssid: string): bool { // Use Nmcli's hasSavedProfile which has the same logic return Nmcli.hasSavedProfile(ssid); @@ -260,39 +309,6 @@ Singleton { return octets.join("."); } - Component.onCompleted: { - // Trigger ethernet device detection after initialization - Qt.callLater(() => { - getEthernetDevices(); - }); - // Load saved connections on startup - Nmcli.loadSavedConnections(() => { - root.savedConnections = Nmcli.savedConnections; - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - }); - // Get initial WiFi status - Nmcli.getWifiStatus(enabled => { - root.wifiEnabled = enabled; - }); - // Sync networks from Nmcli on startup - Qt.callLater(() => { - syncNetworksFromNmcli(); - }, 100); - } - - // Sync saved connections from Nmcli when they're updated - Connections { - function onSavedConnectionsChanged() { - root.savedConnections = Nmcli.savedConnections; - } - - function onSavedConnectionSsidsChanged() { - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - } - - target: Nmcli - } - Timer { id: monitorDebounce @@ -312,21 +328,4 @@ Singleton { onRead: monitorDebounce.start() } } - - Component { - id: apComp - - AccessPoint {} - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } } diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 1f74c97bf..451864710 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -1,10 +1,13 @@ pragma Singleton -import QtQuick +import qs.config + import Quickshell import Quickshell.Io + import Caelestia.Internal -import qs.config + +import QtQuick Singleton { id: root @@ -138,19 +141,16 @@ Singleton { CircularBuffer { id: _downloadBuffer - capacity: root.historyLength + 1 } CircularBuffer { id: _uploadBuffer - capacity: root.historyLength + 1 } FileView { id: netDevFile - path: "/proc/net/dev" } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 7af9513bb..812387f1e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,9 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick import Quickshell import Quickshell.Io +import QtQuick Singleton { id: root @@ -24,14 +24,13 @@ Singleton { property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null + signal connectionFailed(string ssid) property var wirelessDeviceDetails: null property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - property list activeProcesses: [] - readonly property alias connectionCheckTimer: connectionCheckTimer - readonly property alias immediateCheckTimer: immediateCheckTimer + property list activeProcesses: [] // Constants readonly property string deviceTypeWifi: "wifi" @@ -56,8 +55,6 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" - signal connectionFailed(string ssid) - function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; @@ -872,6 +869,247 @@ Singleton { return false; } + component CommandProcess: Process { + id: proc + + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + } + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + + onExited: code => { + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + processFinished(); + } + }); + } + } + + Component { + id: commandProc + + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + function checkPendingConnection(): void { if (root.pendingConnection) { Qt.callLater(() => { @@ -1023,14 +1261,44 @@ Singleton { return details; } - function refreshOnConnectionChange(): void { - getNetworks(networks => { - const newActive = root.active; + Process { + id: rescanProc - if (newActive && newActive.active) { - Qt.callLater(() => { - if (root.wirelessInterfaces.length > 0) { - const activeWireless = root.wirelessInterfaces.find(iface => { + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + onExited: root.getNetworks() + } + + Process { + id: monitorProc + + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } + } + + function refreshOnConnectionChange(): void { + getNetworks(networks => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { @@ -1089,276 +1357,4 @@ Singleton { } }, 2000); } - - Component { - id: commandProc - - CommandProcess {} - } - - Component { - id: apComp - - AccessPoint {} - } - - Timer { - id: connectionCheckTimer - - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - let foundPasswordError = false; - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - foundPasswordError = true; - break; - } - } - } - } - } - - if (!foundPasswordError) { - const pending = root.pendingConnection; - const failedSsid = pending.ssid; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", - exitCode: -1, - needsPassword: false - }); - } - } else if (connected) { - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - } - - Timer { - id: immediateCheckTimer - - property int checkCount: 0 - - interval: 500 - repeat: true - triggeredOnStart: false - - onTriggered: { - if (root.pendingConnection) { - checkCount++; - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (connected) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - if (root.pendingConnection.callback) { - root.pendingConnection.callback({ - success: true, - output: "Connected", - error: "", - exitCode: 0 - }); - } - root.pendingConnection = null; - } else { - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - return; - } - } - } - } - } - - if (checkCount >= 6) { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - - Process { - id: rescanProc - - command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: root.getNetworks() // qmllint disable signal-handler-parameters - } - - Process { - id: monitorProc - - running: true - command: ["nmcli", "monitor"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: SplitParser { - onRead: root.refreshOnConnectionChange() - } - onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters - } - - Timer { - id: monitorRestartTimer - - interval: 2000 - onTriggered: { - monitorProc.running = true; - } - } - - component CommandProcess: Process { - id: proc - - property var callback: null - property list command: [] - property bool callbackCalled: false - property int exitCode: 0 - - signal processFinished - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - - stdout: StdioCollector { - id: stdoutCollector - } - - stderr: StdioCollector { - id: stderrCollector - - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - root.handlePasswordRequired(proc, error, output, -1); - } - } - } - - onExited: code => { // qmllint disable signal-handler-parameters - exitCode = code; - - Qt.callLater(() => { - if (callbackCalled) { - processFinished(); - return; - } - - if (proc.callback) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = exitCode === 0; - const cmdIsConnection = isConnectionCommand(proc.command); - - if (root.handlePasswordRequired(proc, error, output, exitCode)) { - processFinished(); - return; - } - - const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); - - if (!success && cmdIsConnection && root.pendingConnection) { - const failedSsid = root.pendingConnection.ssid; - root.connectionFailed(failedSsid); - } - - callbackCalled = true; - callback({ - success: success, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: needsPassword || false - }); - processFinished(); - } else { - processFinished(); - } - }); - } - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } } diff --git a/services/Notifs.qml b/services/Notifs.qml index 4e6c0dd6b..aff2dfc6a 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -1,22 +1,21 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Services.Notifications -import Caelestia import qs.components.misc -import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications +import QtQuick Singleton { id: root - property list list: [] - readonly property list notClosed: list.filter(n => !n.closed) - readonly property list popups: list.filter(n => n.popup) + property list list: [] + readonly property list notClosed: list.filter(n => !n.closed) + readonly property list popups: list.filter(n => n.popup) property alias dnd: props.dnd property bool loaded @@ -105,9 +104,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "clearNotifs" description: "Clear all notifications" onPressed: { @@ -117,6 +114,8 @@ Singleton { } IpcHandler { + target: "notifs" + function clear(): void { for (const notif of root.list.slice()) notif.close(); @@ -137,13 +136,225 @@ Singleton { function disableDnd(): void { props.dnd = false; } + } - target: "notifs" + component Notif: QtObject { + id: notif + + property bool popup + property bool closed + property var locks: new Set() + + property date time: new Date() + property string timeStr: qsTr("now") + + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } + + readonly property Timer timeStrTimer: Timer { + running: !notif.closed + repeat: true + interval: 5000 + onTriggered: notif.updateTimeStr() + } + + property Notification notification + property string id + property string summary + property string body + property string appIcon + property string appName + property string image + property var hints // Hints are not persisted across restarts + property real expireTimeout: Config.notifs.defaultExpireTimeout + property int urgency: NotificationUrgency.Normal + property bool resident + property bool hasActionIcons + property list actions + + readonly property Timer timer: Timer { + running: true + interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout + onTriggered: { + if (Config.notifs.expire) + notif.popup = false; + } + } + + readonly property LazyLoader dummyImageLoader: LazyLoader { + active: false + + PanelWindow { + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image + color: "transparent" + mask: Region {} + + Image { + function tryCache(): void { + if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) + return; + + const cacheKey = notif.appName + notif.summary + notif.id; + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < cacheKey.length; i++) { + ch = cacheKey.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); + + const cache = `${Paths.notifimagecache}/${hash}.png`; + CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { + notif.image = cache; + notif.dummyImageLoader.active = false; + }); + } + + anchors.fill: parent + source: Qt.resolvedUrl(notif.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + opacity: 0 + + onStatusChanged: tryCache() + onWidthChanged: tryCache() + onHeightChanged: tryCache() + } + } + } + + readonly property Connections conn: Connections { + target: notif.notification + + function onClosed(): void { + notif.close(); + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + if (notif.notification?.image) + notif.dummyImageLoader.active = true; + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onActionsChanged(): void { + notif.actions = notif.notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } + } + + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } + + function close(): void { + closed = true; + if (locks.size === 0 && root.list.includes(this)) { + root.list = root.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } + + Component.onCompleted: { + if (!notification) + return; + + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + if (notification?.image) + dummyImageLoader.active = true; + expireTimeout = notification.expireTimeout; + hints = notification.hints; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + actions = notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } } Component { id: notifComp - NotifData {} + Notif {} } } diff --git a/services/Players.qml b/services/Players.qml index d51b5df38..1191696ae 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -1,12 +1,12 @@ pragma Singleton -import QtQml +import qs.components.misc +import qs.config import Quickshell import Quickshell.Io import Quickshell.Services.Mpris +import QtQml import Caelestia -import qs.components.misc -import qs.config Singleton { id: root @@ -21,16 +21,16 @@ Singleton { } Connections { + target: active + function onPostTrackChanged() { if (!Config.utilities.toasts.nowPlaying) { return; } - if (root.active.trackArtist != "" && root.active.trackTitle != "") { - Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); + if (active.trackArtist != "" && active.trackTitle != "") { + Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); } } - - target: root.active } PersistentProperties { @@ -41,9 +41,7 @@ Singleton { reloadableId: "players" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaToggle" description: "Toggle media playback" onPressed: { @@ -53,9 +51,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaPrev" description: "Previous track" onPressed: { @@ -65,9 +61,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaNext" description: "Next track" onPressed: { @@ -77,15 +71,15 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaStop" description: "Stop media playback" onPressed: root.active?.stop() } IpcHandler { + target: "mpris" + function getActive(prop: string): string { const active = root.active; return active ? active[prop] ?? "Invalid property" : "No active player"; @@ -128,7 +122,5 @@ Singleton { function stop(): void { root.active?.stop(); } - - target: "mpris" } } diff --git a/services/Recorder.qml b/services/Recorder.qml index 8ad00145e..6eddce949 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -1,8 +1,8 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io +import QtQuick Singleton { id: root @@ -46,7 +46,7 @@ Singleton { running: true command: ["pidof", "gpu-screen-recorder"] - onExited: code => { // qmllint disable signal-handler-parameters + onExited: code => { props.running = code === 0; if (code === 0) { @@ -72,11 +72,11 @@ Singleton { } Connections { + target: Time // enabled: props.running && !props.paused + function onSecondsChanged(): void { props.elapsed++; } - - target: Time // qmllint disable incompatible-type } } diff --git a/services/Screens.qml b/services/Screens.qml index 9fed887f3..a64751785 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -1,8 +1,8 @@ pragma Singleton -import Quickshell import qs.config import qs.utils +import Quickshell Singleton { id: root diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 79691fead..508564461 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick +import qs.config import Quickshell import Quickshell.Io -import qs.config +import QtQuick Singleton { id: root @@ -138,7 +138,6 @@ Singleton { Process { id: storage - // Get physical disks with aggregated usage from their partitions // -J triggers JSON output. -b triggers bytes. command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] diff --git a/services/Time.qml b/services/Time.qml index d41c9142c..a07d9ef8e 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -1,8 +1,8 @@ pragma Singleton -import QtQuick -import Quickshell import qs.config +import Quickshell +import QtQuick Singleton { property alias enabled: clock.enabled @@ -23,7 +23,6 @@ Singleton { SystemClock { id: clock - precision: SystemClock.Seconds } } diff --git a/services/VPN.qml b/services/VPN.qml index 2b25813b2..2d08631a1 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io -import Caelestia +import QtQuick import qs.config +import Caelestia Singleton { id: root @@ -128,9 +128,7 @@ Singleton { id: statusProc command: ["ip", "link", "show"] - // qmllint disable incompatible-type environment: ({ - // qmllint enable incompatible-type LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) @@ -145,7 +143,7 @@ Singleton { Process { id: connectProc - onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); @@ -161,7 +159,7 @@ Singleton { Process { id: disconnectProc - onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 391870502..5ddde0c95 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,18 +1,16 @@ pragma Singleton import Quickshell -import qs.components -import qs.services Singleton { property var screens: new Map() property var bars: new Map() - function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { + function load(screen: ShellScreen, visibilities: var): void { screens.set(Hypr.monitorFor(screen), visibilities); } - function getForActive(): DrawerVisibilities { + function getForActive(): PersistentProperties { return screens.get(Hypr.focusedMonitor); } } diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 602abb205..cb96bc565 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -1,12 +1,11 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia.Models -import qs.services import qs.config import qs.utils +import Caelestia.Models +import Quickshell +import Quickshell.Io +import QtQuick Searcher { id: root @@ -47,6 +46,8 @@ Searcher { }) IpcHandler { + target: "wallpaper" + function get(): string { return root.actualCurrent; } @@ -58,8 +59,6 @@ Searcher { function list(): string { return root.list.map(w => w.path).join("\n"); } - - target: "wallpaper" } FileView { diff --git a/services/Weather.qml b/services/Weather.qml index d226365fd..98e29bbba 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick -import Quickshell -import Caelestia import qs.config import qs.utils +import Caelestia +import Quickshell +import QtQuick Singleton { id: root @@ -210,11 +210,10 @@ Singleton { onLocChanged: fetchWeatherData() Connections { + target: Config.services function onWeatherLocationChanged(): void { root.reload(); } - - target: Config.services } // Refresh current location hourly diff --git a/utils/Icons.qml b/utils/Icons.qml index 8a62f62d7..34f8049bf 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick +import qs.config import Quickshell import Quickshell.Services.Notifications -import qs.config +import QtQuick Singleton { id: root diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml index 8331813d9..e55b87bc4 100644 --- a/utils/NetworkConnection.qml +++ b/utils/NetworkConnection.qml @@ -1,7 +1,7 @@ pragma Singleton -import QtQuick import qs.services +import QtQuick /** * NetworkConnection diff --git a/utils/Paths.qml b/utils/Paths.qml index b0926348d..bc89770ab 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -1,9 +1,8 @@ pragma Singleton -import QtQuick -import Quickshell -import Caelestia import qs.config +import Caelestia +import Quickshell Singleton { id: root diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 102c9e766..053b73bba 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -1,7 +1,8 @@ +import Quickshell + import "scripts/fzf.js" as Fzf import "scripts/fuzzysort.js" as Fuzzy import QtQuick -import Quickshell Singleton { required property list list diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 74c94e9bc..19aa4a7a7 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io import qs.config import qs.utils +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root @@ -50,11 +50,11 @@ Singleton { } Connections { + target: Config.general + function onLogoChanged(): void { osRelease.reload(); } - - target: Config.general } Timer {