diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2026-01-03 17:53:06 +1100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-03 17:53:06 +1100 |
| commit | bdcd13222fc6edc77c779a396900ab909e7d5439 (patch) | |
| tree | f9457f3c91c05ec852f974f239d06aca52a3918e /modules/controlcenter | |
| parent | [CI] chore: update flake (diff) | |
| parent | Merge branch 'caelestia-dots:main' into main (diff) | |
| download | caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.gz caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.bz2 caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.zip | |
Merge pull request #906 from atdma/main
controlcenter: many setting panes and minor features
Diffstat (limited to 'modules/controlcenter')
48 files changed, 6773 insertions, 737 deletions
diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 8cdf01f..fdb824e 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -64,6 +64,11 @@ Item { anchors.fill: parent function onWheel(event: WheelEvent): void { + // Prevent tab switching during initial opening animation to avoid blank pages + if (!panes.initialOpeningComplete) { + return; + } + if (event.angleDelta.y < 0) root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); else if (event.angleDelta.y > 0) @@ -76,10 +81,13 @@ Item { screen: root.screen session: root.session + initialOpeningComplete: root.initialOpeningComplete } } Panes { + id: panes + Layout.fillWidth: true Layout.fillHeight: true @@ -88,4 +96,6 @@ Item { session: root.session } } + + readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 22c13a3..ef338b2 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config +import qs.modules.controlcenter import Quickshell import QtQuick import QtQuick.Layouts @@ -12,6 +13,7 @@ Item { required property ShellScreen screen required property Session session + required property bool initialOpeningComplete implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 @@ -30,57 +32,17 @@ Item { PropertyChanges { layout.spacing: Appearance.spacing.small - menuIcon.opacity: 0 - menuIconExpanded.opacity: 1 - menuIcon.rotation: 180 - menuIconExpanded.rotation: 0 } } transitions: Transition { Anim { - properties: "spacing,opacity,rotation" - } - } - - Item { - id: menuBtn - - Layout.topMargin: Appearance.spacing.large - implicitWidth: menuIcon.implicitWidth + menuIcon.anchors.leftMargin * 2 - implicitHeight: menuIcon.implicitHeight + Appearance.padding.normal * 2 - - StateLayer { - radius: Appearance.rounding.small - - function onClicked(): void { - root.session.navExpanded = !root.session.navExpanded; - } - } - - MaterialIcon { - id: menuIcon - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large - - text: "menu" - font.pointSize: Appearance.font.size.large - } - - MaterialIcon { - id: menuIconExpanded - - anchors.fill: menuIcon - text: "menu_open" - font.pointSize: menuIcon.font.pointSize - opacity: 0 - rotation: -180 + properties: "spacing" } } Loader { + Layout.topMargin: Appearance.spacing.large asynchronous: true active: !root.session.floating visible: active @@ -102,7 +64,6 @@ Item { function onClicked(): void { root.session.root.close(); WindowFactory.create(null, { - screen: root.screen, active: root.session.active, navExpanded: root.session.navExpanded }); @@ -156,20 +117,15 @@ Item { } } - NavItem { - Layout.topMargin: Appearance.spacing.large * 2 - icon: "network_manage" - label: "network" - } + Repeater { + model: PaneRegistry.count - NavItem { - icon: "settings_bluetooth" - label: "bluetooth" - } - - NavItem { - icon: "tune" - label: "audio" + NavItem { + required property int index + Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 + icon: PaneRegistry.getByIndex(index).icon + label: PaneRegistry.getByIndex(index).label + } } } @@ -222,6 +178,10 @@ Item { 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) { + return; + } root.session.active = item.label; } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml new file mode 100644 index 0000000..d8bf45e --- /dev/null +++ b/modules/controlcenter/PaneRegistry.qml @@ -0,0 +1,87 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + readonly property list<QtObject> panes: [ + QtObject { + readonly property string id: "network" + readonly property string label: "network" + readonly property string icon: "router" + readonly property string component: "network/NetworkingPane.qml" + }, + QtObject { + readonly property string id: "bluetooth" + readonly property string label: "bluetooth" + readonly property string icon: "settings_bluetooth" + readonly property string component: "bluetooth/BtPane.qml" + }, + QtObject { + readonly property string id: "audio" + readonly property string label: "audio" + readonly property string icon: "volume_up" + readonly property string component: "audio/AudioPane.qml" + }, + QtObject { + readonly property string id: "appearance" + readonly property string label: "appearance" + readonly property string icon: "palette" + readonly property string component: "appearance/AppearancePane.qml" + }, + QtObject { + readonly property string id: "taskbar" + readonly property string label: "taskbar" + readonly property string icon: "task_alt" + readonly property string component: "taskbar/TaskbarPane.qml" + }, + QtObject { + readonly property string id: "launcher" + readonly property string label: "launcher" + readonly property string icon: "apps" + readonly property string component: "launcher/LauncherPane.qml" + } + ] + + readonly property int count: panes.length + + readonly property var labels: { + const result = []; + for (let i = 0; i < panes.length; i++) { + result.push(panes[i].label); + } + return result; + } + + function getByIndex(index: int): QtObject { + if (index >= 0 && index < panes.length) { + return panes[index]; + } + return null; + } + + function getIndexByLabel(label: string): int { + for (let i = 0; i < panes.length; i++) { + if (panes[i].label === label) { + return i; + } + } + return -1; + } + + function getByLabel(label: string): QtObject { + const index = getIndexByLabel(label); + return getByIndex(index); + } + + function getById(id: string): QtObject { + for (let i = 0; i < panes.length; i++) { + if (panes[i].id === id) { + return panes[i]; + } + } + return null; + } +} + diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 2548c3d..833a411 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,9 +1,15 @@ pragma ComponentBehavior: Bound import "bluetooth" +import "network" +import "audio" +import "appearance" +import "taskbar" +import "launcher" import qs.components import qs.services import qs.config +import qs.modules.controlcenter import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -13,74 +19,156 @@ ClippingRectangle { required property Session session + readonly property bool initialOpeningComplete: layout.initialOpeningComplete + color: "transparent" + clip: true + focus: false + activeFocusOnTab: false + + MouseArea { + anchors.fill: parent + z: -1 + onPressed: function(mouse) { + root.focus = true; + mouse.accepted = false; + } + } + + Connections { + target: root.session + + function onActiveIndexChanged(): void { + root.focus = true; + } + } ColumnLayout { id: layout spacing: 0 y: -root.session.activeIndex * root.height + clip: true - Pane { - index: 0 - sourceComponent: Item { - StyledText { - anchors.centerIn: parent - text: qsTr("Work in progress") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } + property bool animationComplete: true + property bool initialOpeningComplete: false + + Timer { + id: animationDelayTimer + interval: Appearance.anim.durations.normal + onTriggered: { + layout.animationComplete = true; } } - Pane { - index: 1 - sourceComponent: BtPane { - session: root.session + Timer { + id: initialOpeningTimer + interval: Appearance.anim.durations.large + running: true + onTriggered: { + layout.initialOpeningComplete = true; } } - Pane { - index: 2 - sourceComponent: Item { - StyledText { - anchors.centerIn: parent - text: qsTr("Work in progress") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } + Repeater { + model: PaneRegistry.count + + Pane { + required property int index + paneIndex: index + componentPath: PaneRegistry.getByIndex(index).component } } Behavior on y { Anim {} } + + Connections { + target: root.session + function onActiveIndexChanged(): void { + layout.animationComplete = false; + animationDelayTimer.restart(); + } + } } component Pane: Item { id: pane - required property int index - property alias sourceComponent: loader.sourceComponent + required property int paneIndex + required property string componentPath implicitWidth: root.width implicitHeight: root.height + property bool hasBeenLoaded: false + + function updateActive(): void { + const diff = Math.abs(root.session.activeIndex - pane.paneIndex); + const isActivePane = diff === 0; + let shouldBeActive = false; + + if (!layout.initialOpeningComplete) { + shouldBeActive = isActivePane; + } else { + if (diff <= 1) { + shouldBeActive = true; + } else if (pane.hasBeenLoaded) { + shouldBeActive = true; + } else { + shouldBeActive = layout.animationComplete; + } + } + + loader.active = shouldBeActive; + } + Loader { id: loader anchors.fill: parent - clip: true + clip: false asynchronous: true - active: { - if (root.session.activeIndex === pane.index) - return true; - - const ly = -layout.y; - const ty = pane.index * root.height; - return ly + root.height > ty && ly < ty + root.height; + active: false + + Component.onCompleted: { + pane.updateActive(); + } + + onActiveChanged: { + if (active && !pane.hasBeenLoaded) { + pane.hasBeenLoaded = true; + } + + if (active && !item) { + loader.setSource(pane.componentPath, { + "session": root.session + }); + } + } + + onItemChanged: { + if (item) { + pane.hasBeenLoaded = true; + } + } + } + + Connections { + target: root.session + function onActiveIndexChanged(): void { + pane.updateActive(); + } + } + + Connections { + target: layout + function onInitialOpeningCompleteChanged(): void { + pane.updateActive(); + } + function onAnimationCompleteChanged(): void { + pane.updateActive(); } } } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index a143470..0408a1a 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,25 +1,21 @@ -import Quickshell.Bluetooth import QtQuick +import "./state" +import qs.modules.controlcenter QtObject { - readonly property list<string> panes: ["network", "bluetooth", "audio"] + readonly property list<string> panes: PaneRegistry.labels required property var root property bool floating: false - property string active: panes[0] + property string active: "network" property int activeIndex: 0 property bool navExpanded: false - readonly property Bt bt: Bt {} + readonly property BluetoothState bt: BluetoothState {} + readonly property NetworkState network: NetworkState {} + readonly property EthernetState ethernet: EthernetState {} + readonly property LauncherState launcher: LauncherState {} onActiveChanged: activeIndex = panes.indexOf(active) onActiveIndexChanged: active = panes[activeIndex] - - component Bt: QtObject { - property BluetoothDevice active - property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter - property bool editingAdapterName - property bool fabMenuOpen - property bool editingDeviceName - } } diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index c5b7535..abcf5df 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -32,12 +32,14 @@ Singleton { destroy(); } - minimumSize.width: 1000 - minimumSize.height: 600 - implicitWidth: cc.implicitWidth implicitHeight: cc.implicitHeight + minimumSize.width: implicitWidth + minimumSize.height: implicitHeight + maximumSize.width: implicitWidth + maximumSize.height: implicitHeight + title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) ControlCenter { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml new file mode 100644 index 0000000..b6acbe5 --- /dev/null +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -0,0 +1,244 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "./sections" +import "../../launcher/services" +import qs.components +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 + + required property Session session + + property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 + property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" + property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" + property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" + property real fontSizeScale: Config.appearance.font.size.scale ?? 1 + property real paddingScale: Config.appearance.padding.scale ?? 1 + property real roundingScale: Config.appearance.rounding.scale ?? 1 + property real spacingScale: Config.appearance.spacing.scale ?? 1 + property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false + property real transparencyBase: Config.appearance.transparency.base ?? 0.85 + property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 + property real borderRounding: Config.border.rounding ?? 1 + property real borderThickness: Config.border.thickness ?? 1 + + property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false + property bool backgroundEnabled: Config.background.enabled ?? true + property bool visualiserEnabled: Config.background.visualiser.enabled ?? false + property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true + 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; + + Config.appearance.font.family.material = root.fontFamilyMaterial; + Config.appearance.font.family.mono = root.fontFamilyMono; + Config.appearance.font.family.sans = root.fontFamilySans; + Config.appearance.font.size.scale = root.fontSizeScale; + + Config.appearance.padding.scale = root.paddingScale; + Config.appearance.rounding.scale = root.roundingScale; + Config.appearance.spacing.scale = root.spacingScale; + + Config.appearance.transparency.enabled = root.transparencyEnabled; + Config.appearance.transparency.base = root.transparencyBase; + Config.appearance.transparency.layers = root.transparencyLayers; + + Config.background.desktopClock.enabled = root.desktopClockEnabled; + Config.background.enabled = root.backgroundEnabled; + + Config.background.visualiser.enabled = root.visualiserEnabled; + Config.background.visualiser.autoHide = root.visualiserAutoHide; + Config.background.visualiser.rounding = root.visualiserRounding; + Config.background.visualiser.spacing = root.visualiserSpacing; + + Config.border.rounding = root.borderRounding; + Config.border.thickness = root.borderThickness; + + Config.save(); + } + + Component { + id: appearanceRightContentComponent + + Item { + id: rightAppearanceFlickable + + ColumnLayout { + id: contentLayout + + anchors.fill: parent + spacing: 0 + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: Appearance.spacing.normal + text: qsTr("Wallpaper") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 + } + + Loader { + id: wallpaperLoader + + Layout.fillWidth: true + 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; + const splitLayout = root.children[0]; + const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null; + const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent); + return shouldActivate; + } + + onStatusChanged: { + if (status === Loader.Error) { + console.error("[AppearancePane] Wallpaper loader error!"); + } + } + + sourceComponent: WallpaperGrid { + session: root.session + } + } + } + } + } + + SplitPaneLayout { + anchors.fill: parent + + leftContent: Component { + + StyledFlickable { + id: sidebarFlickable + readonly property var rootPane: root + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height + + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + 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 + + StyledText { + text: qsTr("Appearance") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + IconButton { + icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true + onClicked: { + const shouldExpand = !sidebarLayout.allSectionsExpanded; + themeModeSection.expanded = shouldExpand; + colorVariantSection.expanded = shouldExpand; + colorSchemeSection.expanded = shouldExpand; + animationsSection.expanded = shouldExpand; + fontsSection.expanded = shouldExpand; + scalesSection.expanded = shouldExpand; + transparencySection.expanded = shouldExpand; + borderSection.expanded = shouldExpand; + backgroundSection.expanded = shouldExpand; + } + } + } + + ThemeModeSection { + id: themeModeSection + } + + ColorVariantSection { + id: colorVariantSection + } + + ColorSchemeSection { + id: colorSchemeSection + } + + 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 + } + } + } + } + + rightContent: appearanceRightContentComponent + } +} diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml new file mode 100644 index 0000000..03fc2b1 --- /dev/null +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Animations") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Animation duration scale") + value: rootPane.animDurationsScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.animDurationsScale = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml new file mode 100644 index 0000000..8754e73 --- /dev/null +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Background") + showBackground: true + + SwitchRow { + label: qsTr("Desktop clock") + checked: rootPane.desktopClockEnabled + onToggled: checked => { + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Background enabled") + checked: rootPane.backgroundEnabled + onToggled: checked => { + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Visualiser") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + SwitchRow { + label: qsTr("Visualiser enabled") + checked: rootPane.visualiserEnabled + onToggled: checked => { + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Visualiser auto hide") + checked: rootPane.visualiserAutoHide + onToggled: checked => { + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Visualiser rounding") + value: rootPane.visualiserRounding + from: 0 + to: 10 + stepSize: 1 + validator: IntValidator { bottom: 0; top: 10 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.visualiserRounding = Math.round(newValue); + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Visualiser spacing") + value: rootPane.visualiserSpacing + from: 0 + to: 2 + validator: DoubleValidator { bottom: 0; top: 2 } + + onValueModified: (newValue) => { + rootPane.visualiserSpacing = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml new file mode 100644 index 0000000..dae26c3 --- /dev/null +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -0,0 +1,63 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Border") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Border rounding") + value: rootPane.borderRounding + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderRounding = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Border thickness") + value: rootPane.borderThickness + from: 0.1 + to: 100 + decimals: 1 + suffix: "px" + validator: DoubleValidator { bottom: 0.1; top: 100 } + + onValueModified: (newValue) => { + rootPane.borderThickness = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml new file mode 100644 index 0000000..c0e5eb5 --- /dev/null +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -0,0 +1,147 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../../launcher/services" +import qs.components +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") + description: qsTr("Available color schemes") + showBackground: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 + + Repeater { + model: Schemes.list + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` + readonly property bool isCurrent: schemeKey === Schemes.currentScheme + + 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 + + StateLayer { + function onClicked(): void { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; + + Schemes.currentScheme = schemeKey; + Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); + + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } + + RowLayout { + id: schemeRow + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + id: preview + + Layout.alignment: Qt.AlignVCenter + + border.width: 1 + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + + color: `#${modelData.colours?.surface}` + radius: Appearance.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth + + MaterialIcon { + id: iconPlaceholder + visible: false + text: "circle" + font.pointSize: Appearance.font.size.large + } + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: parent.implicitWidth / 2 + clip: true + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` + radius: Appearance.rounding.full + } + } + } + + Column { + Layout.fillWidth: true + spacing: 0 + + StyledText { + text: modelData.flavour ?? "" + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: modelData.name ?? "" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + } + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml new file mode 100644 index 0000000..98c3d7c --- /dev/null +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -0,0 +1,92 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../../launcher/services" +import qs.components +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") + description: qsTr("Material theme variant") + showBackground: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small / 2 + + Repeater { + model: M3Variants.list + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary + + StateLayer { + function onClicked(): void { + const variant = modelData.variant; + + Schemes.currentVariant = variant; + Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); + + Qt.callLater(() => { + reloadTimer.restart(); + }); + } + } + + Timer { + id: reloadTimer + interval: 300 + onTriggered: { + Schemes.reload(); + } + } + + RowLayout { + id: variantRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: modelData.icon + font.pointSize: Appearance.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + } + + MaterialIcon { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary + 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 new file mode 100644 index 0000000..57b10ff --- /dev/null +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -0,0 +1,286 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Fonts") + showBackground: true + + CollapsibleSection { + 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: materialFontSection.expanded + + sourceComponent: StyledListView { + id: materialFontList + property alias contentHeight: materialFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: materialFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + 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 + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { + id: monoFontSection + title: qsTr("Monospace font family") + expanded: false + showBackground: true + nested: true + + 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 + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: monoFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + 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 + + StateLayer { + function onClicked(): void { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilyMonoRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { + id: sansFontSection + title: qsTr("Sans-serif font family") + expanded: false + showBackground: true + nested: true + + Loader { + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true + active: sansFontSection.expanded + + sourceComponent: StyledListView { + id: sansFontList + property alias contentHeight: sansFontList.contentHeight + + clip: true + spacing: Appearance.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sansFontList + } + + delegate: StyledRect { + required property string modelData + required property int index + + 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 + + StateLayer { + function onClicked(): void { + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { + id: fontFamilySansRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledText { + text: modelData + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + Loader { + active: isCurrent + asynchronous: true + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + } + } + } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Font size scale") + value: rootPane.fontSizeScale + from: 0.7 + to: 1.5 + decimals: 2 + suffix: "×" + validator: DoubleValidator { bottom: 0.7; top: 1.5 } + + onValueModified: (newValue) => { + rootPane.fontSizeScale = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml new file mode 100644 index 0000000..f74923b --- /dev/null +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Scales") + showBackground: true + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Padding scale") + value: rootPane.paddingScale + from: 0.5 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.5; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.paddingScale = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Rounding scale") + value: rootPane.roundingScale + from: 0.1 + to: 5.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 5.0 } + + onValueModified: (newValue) => { + rootPane.roundingScale = newValue; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Spacing scale") + value: rootPane.spacingScale + from: 0.1 + to: 2.0 + decimals: 1 + suffix: "×" + validator: DoubleValidator { bottom: 0.1; top: 2.0 } + + onValueModified: (newValue) => { + rootPane.spacingScale = newValue; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml new file mode 100644 index 0000000..c136437 --- /dev/null +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -0,0 +1,24 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick + +CollapsibleSection { + title: qsTr("Theme mode") + description: qsTr("Light or dark theme") + showBackground: true + + SwitchRow { + label: qsTr("Dark mode") + checked: !Colours.currentLight + onToggled: checked => { + Colours.setMode(checked ? "dark" : "light"); + } + } +} + diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml new file mode 100644 index 0000000..c9dbfb8 --- /dev/null +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -0,0 +1,74 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +CollapsibleSection { + id: root + + required property var rootPane + + title: qsTr("Transparency") + showBackground: true + + SwitchRow { + label: qsTr("Transparency enabled") + checked: rootPane.transparencyEnabled + onToggled: checked => { + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Transparency base") + value: rootPane.transparencyBase * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyBase = newValue / 100; + rootPane.saveConfig(); + } + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Transparency layers") + value: rootPane.transparencyLayers * 100 + from: 0 + to: 100 + suffix: "%" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + rootPane.transparencyLayers = newValue / 100; + rootPane.saveConfig(); + } + } + } +} + diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml new file mode 100644 index 0000000..76122f9 --- /dev/null +++ b/modules/controlcenter/audio/AudioPane.qml @@ -0,0 +1,467 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +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 + + required property Session session + + anchors.fill: parent + + SplitPaneLayout { + anchors.fill: parent + + leftContent: Component { + + StyledFlickable { + id: leftAudioFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftAudioFlickable + } + + ColumnLayout { + id: leftContent + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Audio") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + CollapsibleSection { + id: outputDevicesSection + + Layout.fillWidth: true + title: qsTr("Output devices") + expanded: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sinks.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + } + + StyledText { + Layout.fillWidth: true + text: qsTr("All available output devices") + color: Colours.palette.m3outline + } + + Repeater { + Layout.fillWidth: true + model: Audio.sinks + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Appearance.rounding.normal + + StateLayer { + function onClicked(): void { + Audio.setAudioSink(modelData); + } + } + + RowLayout { + id: outputRowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" + font.pointSize: Appearance.font.size.large + fill: Audio.sink?.id === modelData.id ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.description || qsTr("Unknown") + font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + } + } + + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { + id: inputDevicesSection + + Layout.fillWidth: true + title: qsTr("Input devices") + expanded: true + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sources.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + } + + StyledText { + Layout.fillWidth: true + text: qsTr("All available input devices") + color: Colours.palette.m3outline + } + + Repeater { + Layout.fillWidth: true + model: Audio.sources + + delegate: StyledRect { + required property var modelData + + Layout.fillWidth: true + + color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Appearance.rounding.normal + + StateLayer { + function onClicked(): void { + Audio.setAudioSource(modelData); + } + } + + RowLayout { + id: inputRowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "mic" + font.pointSize: Appearance.font.size.large + fill: Audio.source?.id === modelData.id ? 1 : 0 + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.description || qsTr("Unknown") + font.weight: Audio.source?.id === modelData.id ? 500 : 400 + } + } + + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + } + } + } + + rightContent: Component { + StyledFlickable { + id: rightAudioFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: rightAudioFlickable + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "volume_up" + title: qsTr("Audio Settings") + } + + SectionHeader { + title: qsTr("Output volume") + description: qsTr("Control the volume of your output device") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + StyledInputField { + id: outputVolumeInput + Layout.preferredWidth: 70 + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.muted + + Component.onCompleted: { + 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); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setVolume(val / 100); + } + } + } + + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.volume * 100).toString(); + } + } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.muted ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { + function onClicked(): void { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } + } + } + + MaterialIcon { + id: muteIcon + + anchors.centerIn: parent + text: Audio.muted ? "volume_off" : "volume_up" + color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } + } + + StyledSlider { + id: outputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + value: Audio.volume + enabled: !Audio.muted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setVolume(value); + if (!outputVolumeInput.hasFocus) { + outputVolumeInput.text = Math.round(value * 100).toString(); + } + } + } + } + } + + SectionHeader { + title: qsTr("Input volume") + description: qsTr("Control the volume of your input device") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + text: qsTr("Volume") + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + StyledInputField { + id: inputVolumeInput + Layout.preferredWidth: 70 + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.sourceMuted + + Component.onCompleted: { + 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); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setSourceVolume(val / 100); + } + } + } + + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.sourceMuted ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { + function onClicked(): void { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } + } + + MaterialIcon { + id: muteInputIcon + + anchors.centerIn: parent + text: "mic_off" + color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } + } + + StyledSlider { + id: inputVolumeSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + value: Audio.sourceVolume + enabled: !Audio.sourceMuted + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setSourceVolume(value); + if (!inputVolumeInput.hasFocus) { + inputVolumeInput.text = Math.round(value * 100).toString(); + } + } + } + } + } + } + } + } + } + }
\ No newline at end of file diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 96dc002..a987e75 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -1,134 +1,73 @@ pragma ComponentBehavior: Bound import ".." -import qs.components.effects +import "../components" +import "." +import qs.components +import qs.components.controls import qs.components.containers import qs.config import Quickshell.Widgets import Quickshell.Bluetooth import QtQuick -import QtQuick.Layouts -RowLayout { +SplitPaneWithDetails { id: root required property Session session anchors.fill: parent - spacing: 0 - - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true - - DeviceList { - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - - session: root.session - } - - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 - } + activeItem: session.bt.active + paneIdGenerator: function(item) { + return item ? (item.address || "") : ""; } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" + leftContent: Component { + StyledFlickable { + id: leftFlickable - Loader { - id: loader + flickableDirection: Flickable.VerticalFlick + contentHeight: deviceList.height - property BluetoothDevice pane: root.session.bt.active - - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - asynchronous: true - sourceComponent: pane ? details : settings + DeviceList { + id: deviceList - Behavior on pane { - SequentialAnimation { - ParallelAnimation { - Anim { - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - Anim { - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - } - PropertyAction {} - ParallelAnimation { - Anim { - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - } - } + anchors.left: parent.left + anchors.right: parent.right + session: root.session } } + } - InnerBorder { - id: rightBorder - - leftThickness: Appearance.padding.normal / 2 + rightDetailsComponent: Component { + Details { + session: root.session } + } - Component { - id: settings - - StyledFlickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - - Settings { - id: settingsInner + rightSettingsComponent: Component { + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height - anchors.left: parent.left - anchors.right: parent.right - session: root.session - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable } - } - Component { - id: details + Settings { + id: settingsInner - Details { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top session: root.session } } } - - component Anim: NumberAnimation { - target: loader - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 104f673..5299045 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -12,409 +13,430 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -Item { +StyledFlickable { id: root required property Session session readonly property BluetoothDevice device: session.bt.active - StyledFlickable { - anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: detailsWrapper.height - flickableDirection: Flickable.VerticalFlick - contentHeight: layout.height + StyledScrollBar.vertical: StyledScrollBar { + flickable: root + } + + Item { + id: detailsWrapper + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: details.implicitHeight - ColumnLayout { - id: layout + DeviceDetails { + id: details anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + anchors.top: parent.top - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - animate: true - text: Icons.getBluetoothIcon(root.device.icon) - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } + session: root.session + device: root.device - StyledText { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.device?.name ?? "" - font.pointSize: Appearance.font.size.large - font.bold: true + headerComponent: Component { + SettingsHeader { + icon: Icons.getBluetoothIcon(root.device?.icon ?? "") + title: root.device?.name ?? "" + } } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - StyledText { - text: qsTr("Connection settings for this device") - color: Colours.palette.m3outline - } + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Connection status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Connection settings for this device") + color: Colours.palette.m3outline + } - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - ColumnLayout { - id: deviceStatus + ColumnLayout { + id: deviceStatus - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.larger + spacing: Appearance.spacing.larger - Toggle { - label: qsTr("Connected") - checked: root.device?.connected ?? false - toggle.onToggled: root.device.connected = checked - } + Toggle { + label: qsTr("Connected") + checked: root.device?.connected ?? false + toggle.onToggled: root.device.connected = checked + } + + Toggle { + label: qsTr("Paired") + checked: root.device?.paired ?? false + toggle.onToggled: { + if (root.device.paired) + root.device.forget(); + else + root.device.pair(); + } + } - Toggle { - label: qsTr("Paired") - checked: root.device?.paired ?? false - toggle.onToggled: { - if (root.device.paired) - root.device.forget(); - else - root.device.pair(); + Toggle { + label: qsTr("Blocked") + checked: root.device?.blocked ?? false + toggle.onToggled: root.device.blocked = checked + } + } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - Toggle { - label: qsTr("Blocked") - checked: root.device?.blocked ?? false - toggle.onToggled: root.device.blocked = checked - } - } - } + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Device properties") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledText { + text: qsTr("Additional settings") + color: Colours.palette.m3outline + } - StyledText { - text: qsTr("Additional settings") - color: Colours.palette.m3outline - } + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + ColumnLayout { + id: deviceProps - ColumnLayout { - id: deviceProps + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.larger - spacing: Appearance.spacing.larger + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small + Item { + id: renameDevice - Item { - id: renameDevice + Layout.fillWidth: true + Layout.rightMargin: Appearance.spacing.small - Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight - implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight + states: State { + name: "editingDeviceName" + when: root.session.bt.editingDeviceName - states: State { - name: "editingDeviceName" - when: root.session.bt.editingDeviceName + AnchorChanges { + target: deviceNameEdit + anchors.top: renameDevice.top + } + PropertyChanges { + renameDevice.implicitHeight: deviceNameEdit.implicitHeight + renameLabel.opacity: 0 + deviceNameEdit.padding: Appearance.padding.normal + } + } - AnchorChanges { - target: deviceNameEdit - anchors.top: renameDevice.top - } - PropertyChanges { - renameDevice.implicitHeight: deviceNameEdit.implicitHeight - renameLabel.opacity: 0 - deviceNameEdit.padding: Appearance.padding.normal - } - } + transitions: Transition { + AnchorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + Anim { + properties: "implicitHeight,opacity,padding" + } + } - transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard - } - Anim { - properties: "implicitHeight,opacity,padding" - } - } + StyledText { + id: renameLabel - StyledText { - id: renameLabel + anchors.left: parent.left - anchors.left: parent.left + text: qsTr("Device name") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - text: qsTr("Device name") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + StyledTextField { + id: deviceNameEdit - StyledTextField { - id: deviceNameEdit + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal - anchors.left: parent.left - anchors.right: parent.right - anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal + text: root.device?.name ?? "" + readOnly: !root.session.bt.editingDeviceName + onAccepted: { + root.session.bt.editingDeviceName = false; + root.device.name = text; + } - text: root.device?.name ?? "" - readOnly: !root.session.bt.editingDeviceName - onAccepted: { - root.session.bt.editingDeviceName = false; - root.device.name = text; - } + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + background: StyledRect { + radius: Appearance.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingDeviceName ? 1 : 0 - background: StyledRect { - radius: Appearance.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingDeviceName ? 1 : 0 + Behavior on border.color { + CAnim {} + } - Behavior on border.color { - CAnim {} - } + Behavior on opacity { + Anim {} + } + } - Behavior on opacity { - Anim {} + Behavior on anchors.leftMargin { + Anim {} + } + } } - } - Behavior on anchors.leftMargin { - Anim {} - } - } - } + StyledRect { + implicitWidth: implicitHeight + implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 - StyledRect { - implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + radius: Appearance.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingDeviceName ? 1 : 0 + scale: root.session.bt.editingDeviceName ? 1 : 0.5 - radius: Appearance.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingDeviceName ? 1 : 0 - scale: root.session.bt.editingDeviceName ? 1 : 0.5 + StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName - 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 ?? ""); + } + } - function onClicked(): void { - root.session.bt.editingDeviceName = false; - deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); - } - } + MaterialIcon { + id: cancelEditIcon - MaterialIcon { - id: cancelEditIcon + anchors.centerIn: parent + animate: true + text: "cancel" + color: Colours.palette.m3onSecondaryContainer + } - anchors.centerIn: parent - animate: true - text: "cancel" - color: Colours.palette.m3onSecondaryContainer - } + Behavior on opacity { + Anim {} + } - Behavior on opacity { - Anim {} - } + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } + StyledRect { + implicitWidth: implicitHeight + implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 - StyledRect { - implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) - radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) + StateLayer { + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - 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) + deviceNameEdit.forceActiveFocus(); + else + deviceNameEdit.accepted(); + } + } - function onClicked(): void { - root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; - if (root.session.bt.editingDeviceName) - deviceNameEdit.forceActiveFocus(); - else - deviceNameEdit.accepted(); - } - } + MaterialIcon { + id: editIcon - MaterialIcon { - id: editIcon + anchors.centerIn: parent + animate: true + text: root.session.bt.editingDeviceName ? "check_circle" : "edit" + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + } - anchors.centerIn: parent - animate: true - text: root.session.bt.editingDeviceName ? "check_circle" : "edit" - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } + Behavior on radius { + Anim {} + } + } + } - Behavior on radius { - Anim {} + Toggle { + label: qsTr("Trusted") + checked: root.device?.trusted ?? false + toggle.onToggled: root.device.trusted = checked + } + + Toggle { + label: qsTr("Wake allowed") + checked: root.device?.wakeAllowed ?? false + toggle.onToggled: root.device.wakeAllowed = checked + } } } } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal - Toggle { - label: qsTr("Trusted") - checked: root.device?.trusted ?? false - toggle.onToggled: root.device.trusted = checked - } - - Toggle { - label: qsTr("Wake allowed") - checked: root.device?.wakeAllowed ?? false - toggle.onToggled: root.device.wakeAllowed = checked - } - } - } - - StyledText { - Layout.topMargin: Appearance.spacing.large - text: qsTr("Device information") - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } - StyledText { - text: qsTr("Information about this device") - color: Colours.palette.m3outline - } + StyledText { + text: qsTr("Information about this device") + color: Colours.palette.m3outline + } - StyledRect { - Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer - ColumnLayout { - id: deviceInfo + ColumnLayout { + id: deviceInfo - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Appearance.spacing.small / 2 - StyledText { - text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") - } + StyledText { + text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") + } - RowLayout { - Layout.topMargin: Appearance.spacing.small / 2 - Layout.fillWidth: true - Layout.preferredHeight: Appearance.padding.smaller - spacing: Appearance.spacing.small / 2 + RowLayout { + Layout.topMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.smaller + spacing: Appearance.spacing.small / 2 - StyledRect { - Layout.fillHeight: true - implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 - radius: Appearance.rounding.full - color: Colours.palette.m3primary - } + StyledRect { + Layout.fillHeight: true + implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true - radius: Appearance.rounding.full - color: Colours.palette.m3secondaryContainer + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Appearance.rounding.full + color: Colours.palette.m3secondaryContainer - StyledRect { - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: parent.height * 0.25 + StyledRect { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: parent.height * 0.25 - implicitWidth: height - radius: Appearance.rounding.full - color: Colours.palette.m3primary - } - } - } + implicitWidth: height + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } + } + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Dbus path") - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Dbus path") + } - StyledText { - text: root.device?.dbusPath ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + StyledText { + text: root.device?.dbusPath ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("MAC address") - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("MAC address") + } - StyledText { - text: root.device?.address ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("Bonded") - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Bonded") + } - StyledText { - text: root.device?.bonded ? qsTr("Yes") : qsTr("No") - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - } + StyledText { + text: root.device?.bonded ? qsTr("Yes") : qsTr("No") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } - StyledText { - Layout.topMargin: Appearance.spacing.normal - text: qsTr("System name") - } + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("System name") + } - StyledText { - text: root.device?.deviceName ?? "" - color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + StyledText { + text: root.device?.deviceName ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } } } - } + ] } } @@ -562,11 +584,11 @@ Item { Item { id: fabRoot - anchors.right: parent.right - anchors.bottom: parent.bottom - - implicitWidth: 64 - implicitHeight: 64 + x: root.contentX + root.width - width + y: root.contentY + root.height - height + width: 64 + height: 64 + z: 10000 StyledRect { id: fabBg diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 3831e4a..b978a2d 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.containers @@ -12,172 +13,142 @@ import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -ColumnLayout { +DeviceList { id: root required property Session session readonly property bool smallDiscoverable: width <= 540 readonly property bool smallPairable: width <= 480 - spacing: Appearance.spacing.small + title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) + description: qsTr("All available bluetooth devices") + activeItem: session.bt.active - RowLayout { - spacing: Appearance.spacing.smaller + model: ScriptModel { + id: deviceModel - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - Item { - Layout.fillWidth: true - } + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) + } - ToggleButton { - toggled: Bluetooth.defaultAdapter?.enabled ?? false - icon: "power" - accent: "Tertiary" + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = !adapter.enabled; + StyledText { + text: qsTr("Bluetooth") + font.pointSize: Appearance.font.size.large + font.weight: 500 } - } - - ToggleButton { - toggled: Bluetooth.defaultAdapter?.discoverable ?? false - icon: root.smallDiscoverable ? "group_search" : "" - label: root.smallDiscoverable ? "" : qsTr("Discoverable") - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.discoverable = !adapter.discoverable; + Item { + Layout.fillWidth: true } - } - ToggleButton { - toggled: Bluetooth.defaultAdapter?.pairable ?? false - icon: "missing_controller" - label: root.smallPairable ? "" : qsTr("Pairable") + ToggleButton { + toggled: Bluetooth.defaultAdapter?.enabled ?? false + icon: "power" + accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Toggle Bluetooth") - function onClicked(): void { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.pairable = !adapter.pairable; + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = !adapter.enabled; + } } - } - ToggleButton { - toggled: !root.session.bt.active - icon: "settings" - accent: "Primary" + ToggleButton { + toggled: Bluetooth.defaultAdapter?.discoverable ?? false + icon: root.smallDiscoverable ? "group_search" : "" + label: root.smallDiscoverable ? "" : qsTr("Discoverable") + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Make discoverable") - function onClicked(): void { - if (root.session.bt.active) - root.session.bt.active = null; - else { - root.session.bt.active = deviceModel.values[0] ?? null; + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discoverable = !adapter.discoverable; } } - } - } - RowLayout { - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - spacing: Appearance.spacing.normal + ToggleButton { + toggled: Bluetooth.defaultAdapter?.pairable ?? false + icon: "missing_controller" + label: root.smallPairable ? "" : qsTr("Pairable") + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Make pairable") - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.small - - StyledText { - Layout.fillWidth: true - text: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) - font.pointSize: Appearance.font.size.large - font.weight: 500 - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available bluetooth devices") - color: Colours.palette.m3outline + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.pairable = !adapter.pairable; + } } - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + ToggleButton { + toggled: Bluetooth.defaultAdapter?.discovering ?? false + icon: "bluetooth_searching" + accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Scan for devices") - StateLayer { - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - - function onClicked(): void { + onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discovering = !adapter.discovering; } } - MaterialIcon { - id: scanIcon - - anchors.centerIn: parent - animate: true - text: "bluetooth_searching" - color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 - } + ToggleButton { + toggled: !root.session.bt.active + icon: "settings" + accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Bluetooth settings") - Behavior on radius { - Anim {} + onClicked: { + if (root.session.bt.active) + root.session.bt.active = null; + else { + root.session.bt.active = root.model.values[0] ?? null; + } + } } } } - StyledListView { - id: view - - model: ScriptModel { - id: deviceModel - - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) - } - - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - spacing: Appearance.spacing.small / 2 - - StyledScrollBar.vertical: StyledScrollBar { - flickable: view - } - delegate: StyledRect { + delegate: Component { + StyledRect { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting - readonly property bool connected: modelData.state === BluetoothDeviceState.Connected + readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) + readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected - anchors.left: parent.left - anchors.right: parent.right + width: ListView.view ? ListView.view.width : undefined implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.bt.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { id: stateLayer function onClicked(): void { - root.session.bt.active = device.modelData; + if (device.modelData) + root.session.bt.active = device.modelData; } } @@ -194,20 +165,20 @@ ColumnLayout { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: device.connected ? Colours.palette.m3primaryContainer : device.modelData.bonded ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius - color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) } MaterialIcon { id: icon anchors.centerIn: parent - text: Icons.getBluetoothIcon(device.modelData.icon) - color: device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") + color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.large fill: device.connected ? 1 : 0 @@ -224,13 +195,13 @@ ColumnLayout { StyledText { Layout.fillWidth: true - text: device.modelData.name + text: device.modelData ? device.modelData.name : qsTr("Unknown") elide: Text.ElideRight } StyledText { Layout.fillWidth: true - text: device.modelData.address + (device.connected ? qsTr(" (Connected)") : device.modelData.bonded ? qsTr(" (Paired)") : "") + text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight @@ -256,7 +227,18 @@ ColumnLayout { disabled: device.loading function onClicked(): void { - device.modelData.connected = !device.modelData.connected; + if (device.loading) + return; + + if (device.connected) { + device.modelData.connected = false; + } else { + if (device.modelData.bonded) { + device.modelData.connected = true; + } else { + device.modelData.pair(); + } + } } } @@ -265,7 +247,7 @@ ColumnLayout { anchors.centerIn: parent animate: true - text: device.modelData.connected ? "link_off" : "link" + text: device.connected ? "link_off" : "link" color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 @@ -279,78 +261,7 @@ ColumnLayout { } } - component ToggleButton: StyledRect { - id: toggleBtn - - required property bool toggled - property string icon - property string label - property string accent: "Secondary" - - function onClicked(): void { - } - - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 - implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 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: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - - function onClicked(): void { - toggleBtn.onClicked(); - } - } - - RowLayout { - id: toggleBtnInner - - anchors.centerIn: parent - spacing: Appearance.spacing.normal - - MaterialIcon { - id: toggleBtnIcon - - visible: !!text - fill: toggleBtn.toggled ? 1 : 0 - text: toggleBtn.icon - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] - font.pointSize: Appearance.font.size.large - - Behavior on fill { - Anim {} - } - } - - Loader { - asynchronous: true - active: !!toggleBtn.label - visible: active - - sourceComponent: StyledText { - text: toggleBtn.label - color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.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 - } - } + onItemSelected: function(item) { + session.bt.active = item; } } diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index fb493ff..c547240 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects @@ -17,18 +18,9 @@ ColumnLayout { spacing: Appearance.spacing.normal - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "bluetooth" - font.pointSize: Appearance.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Bluetooth settings") - font.pointSize: Appearance.font.size.large - font.bold: true + SettingsHeader { + icon: "bluetooth" + title: qsTr("Bluetooth Settings") } StyledText { @@ -284,8 +276,12 @@ ColumnLayout { CustomSpinBox { min: 0 - value: root.session.bt.currentAdapter.discoverableTimeout - onValueModified: value => root.session.bt.currentAdapter.discoverableTimeout = value + value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0 + onValueModified: value => { + if (root.session.bt.currentAdapter) { + root.session.bt.currentAdapter.discoverableTimeout = value; + } + } } } @@ -332,7 +328,7 @@ ColumnLayout { anchors.left: parent.left - text: qsTr("Rename adapter (currently does not work)") // FIXME: remove disclaimer when fixed + text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } @@ -345,12 +341,10 @@ ColumnLayout { anchors.top: renameLabel.bottom anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal - text: root.session.bt.currentAdapter.name + text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName onAccepted: { root.session.bt.editingAdapterName = false; - // Doesn't work for now, will be added to QS later - // root.session.bt.currentAdapter.name = text; } leftPadding: Appearance.padding.normal @@ -392,7 +386,7 @@ ColumnLayout { function onClicked(): void { root.session.bt.editingAdapterName = false; - adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter.name); + adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } } diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml new file mode 100644 index 0000000..8cc9177 --- /dev/null +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -0,0 +1,71 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + property Session session + property var device: null + + property Component headerComponent: null + property list<Component> sections: [] + + property Component topContent: null + property Component bottomContent: null + + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + Loader { + id: headerLoader + + Layout.fillWidth: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null + } + + Loader { + id: topContentLoader + + Layout.fillWidth: true + sourceComponent: root.topContent + visible: root.topContent !== null + } + + Repeater { + model: root.sections + + Loader { + required property Component modelData + + Layout.fillWidth: true + sourceComponent: modelData + } + } + + Loader { + id: bottomContentLoader + + Layout.fillWidth: true + sourceComponent: root.bottomContent + visible: root.bottomContent !== null + } + } +} + diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml new file mode 100644 index 0000000..75dd913 --- /dev/null +++ b/modules/controlcenter/components/DeviceList.qml @@ -0,0 +1,85 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + property Session session: null + property var model: null + property Component delegate: null + + property string title: "" + property string description: "" + property var activeItem: null + property Component headerComponent: null + property Component titleSuffix: null + property bool showHeader: true + + signal itemSelected(var item) + + spacing: Appearance.spacing.small + + Loader { + id: headerLoader + + Layout.fillWidth: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null && root.showHeader + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: root.headerComponent ? 0 : 0 + spacing: Appearance.spacing.small + visible: root.title !== "" || root.description !== "" + + StyledText { + visible: root.title !== "" + text: root.title + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Loader { + sourceComponent: root.titleSuffix + visible: root.titleSuffix !== null + } + + Item { + Layout.fillWidth: true + } + } + + property alias view: view + + StyledText { + visible: root.description !== "" + Layout.fillWidth: true + text: root.description + color: Colours.palette.m3outline + } + + StyledListView { + id: view + + Layout.fillWidth: true + implicitHeight: contentHeight + + model: root.model + delegate: root.delegate + + spacing: Appearance.spacing.small / 2 + interactive: false + clip: false + } +} + diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml new file mode 100644 index 0000000..d1814b5 --- /dev/null +++ b/modules/controlcenter/components/PaneTransition.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound + +import qs.config +import QtQuick + +SequentialAnimation { + id: root + + required property Item target + property list<PropertyAction> propertyActions + + property real scaleFrom: 1.0 + property real scaleTo: 0.8 + property real opacityFrom: 1.0 + property real opacityTo: 0.0 + + ParallelAnimation { + NumberAnimation { + target: root.target + property: "opacity" + from: root.opacityFrom + to: root.opacityTo + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + + NumberAnimation { + target: root.target + property: "scale" + from: root.scaleFrom + to: root.scaleTo + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + + ScriptAction { + script: { + for (let i = 0; i < root.propertyActions.length; i++) { + const action = root.propertyActions[i]; + if (action.target && action.property !== undefined) { + action.target[action.property] = action.value; + } + } + } + } + + ParallelAnimation { + NumberAnimation { + target: root.target + property: "opacity" + from: root.opacityTo + to: root.opacityFrom + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + + NumberAnimation { + target: root.target + property: "scale" + from: root.scaleTo + to: root.scaleFrom + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } +} + diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml new file mode 100644 index 0000000..c1ba148 --- /dev/null +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -0,0 +1,38 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property string icon + required property string title + + Layout.fillWidth: true + implicitHeight: column.implicitHeight + + ColumnLayout { + id: column + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.title + font.pointSize: Appearance.font.size.large + font.bold: true + } + } +} + diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml new file mode 100644 index 0000000..7348368 --- /dev/null +++ b/modules/controlcenter/components/SliderInput.qml @@ -0,0 +1,181 @@ +pragma ComponentBehavior: Bound + +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 + + property string label: "" + property real value: 0 + property real from: 0 + property real to: 100 + property real stepSize: 0 + property var validator: null + property string suffix: "" // Optional suffix text (e.g., "×", "px") + 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 + + function formatValue(val: real): string { + if (formatValueFunction) { + return formatValueFunction(val); + } + // Default format function + // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property) + if (validator && validator.bottom !== undefined && validator.decimals === undefined) { + return Math.round(val).toString(); + } + // For DoubleValidator or no validator, use the decimals property + return val.toFixed(root.decimals); + } + + function parseValue(text: string): real { + if (parseValueFunction) { + return parseValueFunction(text); + } + // Default parse function + if (validator && validator.bottom !== undefined) { + // Check if it's an integer validator + if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { + return parseInt(text); + } + } + return parseFloat(text); + } + + signal valueModified(real newValue) + + property bool _initialized: false + + spacing: Appearance.spacing.small + + Component.onCompleted: { + // Set initialized flag after a brief delay to allow component to fully load + Qt.callLater(() => { + _initialized = true; + }); + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + StyledInputField { + id: inputField + Layout.preferredWidth: 70 + validator: root.validator + + Component.onCompleted: { + // Initialize text without triggering valueModified signal + text = root.formatValue(root.value); + } + + onTextEdited: (text) => { + if (hasFocus) { + const val = root.parseValue(text); + if (!isNaN(val)) { + // Validate against validator bounds if available + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isValid) { + root.valueModified(val); + } + } + } + } + + onEditingFinished: { + const val = root.parseValue(text); + let isValid = true; + if (root.validator) { + if (root.validator.bottom !== undefined && val < root.validator.bottom) { + isValid = false; + } + if (root.validator.top !== undefined && val > root.validator.top) { + isValid = false; + } + } + + if (isNaN(val) || !isValid) { + text = root.formatValue(root.value); + } + } + } + + StyledText { + visible: root.suffix !== "" + text: root.suffix + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + } + + StyledSlider { + id: slider + + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + from: root.from + 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) + if (!inputField.hasFocus) { + const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; + inputField.text = root.formatValue(newValue); + } + } + + onMoved: { + const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; + root.valueModified(newValue); + if (!inputField.hasFocus) { + inputField.text = root.formatValue(newValue); + } + } + } + + // 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 new file mode 100644 index 0000000..8b4f0d9 --- /dev/null +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -0,0 +1,112 @@ +pragma ComponentBehavior: Bound + +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 + + Item { + id: leftPane + + Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio) + Layout.minimumWidth: root.leftMinimumWidth + Layout.fillHeight: true + + ClippingRectangle { + id: leftClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: leftBorder.innerRadius + color: "transparent" + + Loader { + id: leftLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + asynchronous: true + sourceComponent: root.leftContent + + Component.onCompleted: { + for (const key in root.leftLoaderProperties) { + leftLoader[key] = root.leftLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: leftBorder + + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + id: rightPane + + Layout.fillWidth: true + Layout.fillHeight: true + + ClippingRectangle { + id: rightClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" + + Loader { + id: rightLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + asynchronous: true + sourceComponent: root.rightContent + + Component.onCompleted: { + for (const key in root.rightLoaderProperties) { + rightLoader[key] = root.rightLoaderProperties[key]; + } + } + } + } + + InnerBorder { + id: rightBorder + + leftThickness: Appearance.padding.normal / 2 + } + } +} + diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml new file mode 100644 index 0000000..e873923 --- /dev/null +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -0,0 +1,93 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Component leftContent + required property Component rightDetailsComponent + required property Component rightSettingsComponent + + property var activeItem: null + property var paneIdGenerator: function(item) { return item ? String(item) : ""; } + + property Component overlayComponent: null + + SplitPaneLayout { + id: splitLayout + + anchors.fill: parent + + leftContent: root.leftContent + + rightContent: Component { + Item { + id: rightPaneItem + + property var pane: root.activeItem + property string paneId: root.paneIdGenerator(pane) + property Component targetComponent: root.rightSettingsComponent + property Component nextComponent: root.rightSettingsComponent + + function getComponentForPane() { + return pane ? root.rightDetailsComponent : root.rightSettingsComponent; + } + + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } + + Loader { + id: rightLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: false + asynchronous: true + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } + } + } + } + + Loader { + id: overlayLoader + + anchors.fill: parent + z: 1000 + sourceComponent: root.overlayComponent + active: root.overlayComponent !== null + } +} + diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml new file mode 100644 index 0000000..5eab5b8 --- /dev/null +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -0,0 +1,241 @@ +pragma ComponentBehavior: Bound + +import ".." +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 + + required property Session session + + readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) + + cellWidth: width / columnsCount + cellHeight: 140 + Appearance.spacing.normal + + model: Wallpapers.list + + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: root + } + + delegate: Item { + required property var modelData + required property int index + + width: root.cellWidth + height: root.cellHeight + + 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 { + id: image + + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + color: Colours.tPalette.m3surfaceContainer + radius: itemRadius + antialiasing: true + layer.enabled: true + layer.smooth: true + + CachingImage { + id: cachingImage + + path: modelData.path + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 + antialiasing: true + smooth: true + sourceSize: Qt.size(width, height) + + opacity: status === Image.Ready ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad + } + } + } + + // Fallback if CachingImage fails to load + Image { + id: fallbackImage + + anchors.fill: parent + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" + asynchronous: true + fillMode: Image.PreserveAspectCrop + cache: true + visible: opacity > 0 + antialiasing: true + smooth: true + sourceSize: Qt.size(width, height) + + opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutQuad + } + } + } + + Timer { + id: fallbackTimer + + property bool triggered: false + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true + } + + // Gradient overlay for filename + Rectangle { + id: filenameOverlay + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 + radius: 0 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0) + } + GradientStop { + position: 0.3 + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.7) + } + GradientStop { + position: 0.6 + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.9) + } + GradientStop { + position: 1.0 + color: Qt.rgba(Colours.palette.m3surface.r, + Colours.palette.m3surface.g, + Colours.palette.m3surface.b, 0.95) + } + } + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } + } + } + + Rectangle { + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + color: "transparent" + radius: itemRadius + border.width + border.width: isCurrent ? 2 : 0 + border.color: Colours.palette.m3primary + antialiasing: true + smooth: true + + Behavior on border.width { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + MaterialIcon { + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.small + + visible: isCurrent + text: "check_circle" + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.large + } + } + + StyledText { + id: filenameText + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 + anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 + anchors.bottomMargin: Appearance.padding.normal + + text: modelData.name + font.pointSize: Appearance.font.size.smaller + font.weight: 500 + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + elide: Text.ElideMiddle + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } + } + } + } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml new file mode 100644 index 0000000..47f87cc --- /dev/null +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -0,0 +1,599 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "../../launcher/services" +import qs.components +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 + + required property Session session + + property var selectedApp: root.session.launcher.active + property bool hideFromLauncherChecked: false + + 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) { + root.hideFromLauncherChecked = false; + return; + } + + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + + if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { + root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); + } else { + root.hideFromLauncherChecked = false; + } + } + + function saveHiddenApps(isHidden) { + if (!root.selectedApp) { + return; + } + + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + + const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + + if (isHidden) { + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); + } + } + + Config.launcher.hiddenApps = hiddenApps; + Config.save(); + } + + + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + entries: DesktopEntries.applications.values + } + + property string searchText: "" + + function filterApps(search: string): list<var> { + if (!search || search.trim() === "") { + const apps = []; + for (let i = 0; i < allAppsDb.apps.length; i++) { + apps.push(allAppsDb.apps[i]); + } + return apps; + } + + if (!allAppsDb.apps || allAppsDb.apps.length === 0) { + return []; + } + + const preparedApps = []; + for (let i = 0; i < allAppsDb.apps.length; i++) { + const app = allAppsDb.apps[i]; + const name = app.name || app.entry?.name || ""; + preparedApps.push({ + _item: app, + name: Fuzzy.prepare(name) + }); + } + + const results = Fuzzy.go(search, preparedApps, { + all: true, + keys: ["name"], + scoreFn: r => r[0].score + }); + + return results + .sort((a, b) => b._score - a._score) + .map(r => r.obj._item); + } + + property list<var> filteredApps: [] + + function updateFilteredApps() { + filteredApps = filterApps(searchText); + } + + onSearchTextChanged: { + updateFilteredApps(); + } + + Component.onCompleted: { + updateFilteredApps(); + } + + Connections { + target: allAppsDb + function onAppsChanged() { + updateFilteredApps(); + } + } + + SplitPaneLayout { + anchors.fill: parent + + leftContent: Component { + + ColumnLayout { + id: leftLauncherLayout + anchors.fill: parent + + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Launcher") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: !root.session.launcher.active + icon: "settings" + accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Launcher settings") + + onClicked: { + if (root.session.launcher.active) { + root.session.launcher.active = null; + } else { + if (root.filteredApps.length > 0) { + root.session.launcher.active = root.filteredApps[0]; + } + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledText { + text: qsTr("All applications available in the launcher") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.bottomMargin: Appearance.spacing.small + + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.full + + implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) + + MaterialIcon { + id: searchIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.normal + + text: "search" + color: Colours.palette.m3onSurfaceVariant + } + + StyledTextField { + id: searchField + + anchors.left: searchIcon.right + anchors.right: clearIcon.left + anchors.leftMargin: Appearance.spacing.small + anchors.rightMargin: Appearance.spacing.small + + topPadding: Appearance.padding.normal + bottomPadding: Appearance.padding.normal + + placeholderText: qsTr("Search applications...") + + onTextChanged: { + root.searchText = text; + } + } + + MaterialIcon { + id: clearIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + + width: searchField.text ? implicitWidth : implicitWidth / 2 + opacity: { + if (!searchField.text) + return 0; + if (clearMouse.pressed) + return 0.7; + if (clearMouse.containsMouse) + return 0.8; + return 1; + } + + text: "close" + color: Colours.palette.m3onSurfaceVariant + + MouseArea { + id: clearMouse + + anchors.fill: parent + hoverEnabled: true + cursorShape: searchField.text ? Qt.PointingHandCursor : undefined + + onClicked: searchField.text = "" + } + + Behavior on width { + Anim { + duration: Appearance.anim.durations.small + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + } + } + + Loader { + id: appsListLoader + Layout.fillWidth: true + Layout.fillHeight: true + asynchronous: true + active: true + + sourceComponent: StyledListView { + id: appsListView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: root.filteredApps + spacing: Appearance.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + delegate: StyledRect { + required property var modelData + + width: parent ? parent.width : 0 + + readonly property bool isSelected: root.selectedApp === modelData + + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Appearance.rounding.normal + + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + opacity = 1; + } + + StateLayer { + function onClicked(): void { + root.session.launcher.active = modelData; + } + } + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + IconImage { + Layout.alignment: Qt.AlignVCenter + implicitSize: 32 + source: { + const entry = modelData.entry; + return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; + } + } + + StyledText { + Layout.fillWidth: true + text: modelData.name || modelData.entry?.name || qsTr("Unknown") + font.pointSize: Appearance.font.size.normal + } + } + + implicitHeight: 40 + } + } + } + } + } + + rightContent: Component { + Item { + id: rightLauncherPane + + property var pane: root.session.launcher.active + property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" + property Component targetComponent: settings + property Component nextComponent: settings + property var displayedApp: null + + function getComponentForPane() { + return pane ? appDetails : settings; + } + + Component.onCompleted: { + displayedApp = pane; + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } + + Loader { + id: rightLauncherLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + clip: false + + asynchronous: true + sourceComponent: rightLauncherPane.targetComponent + active: true + + property var displayedApp: rightLauncherPane.displayedApp + + onItemChanged: { + if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { + rightLauncherPane.displayedApp = rightLauncherPane.pane; + } + } + } + + Behavior on paneId { + PaneTransition { + target: rightLauncherLoader + propertyActions: [ + PropertyAction { + target: rightLauncherPane + property: "displayedApp" + value: rightLauncherPane.pane + }, + PropertyAction { + target: rightLauncherLoader + property: "active" + value: false + }, + PropertyAction { + target: rightLauncherPane + property: "targetComponent" + value: rightLauncherPane.nextComponent + }, + PropertyAction { + target: rightLauncherLoader + property: "active" + value: true + } + ] + } + } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { + root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); + } else { + root.hideFromLauncherChecked = false; + } + } else { + root.hideFromLauncherChecked = false; + } + } + } + } + } + + Component { + id: settings + + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } + + Settings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session + } + } + } + + Component { + id: appDetails + + ColumnLayout { + id: appDetailsLayout + anchors.fill: parent + + readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null + + spacing: Appearance.spacing.normal + + SettingsHeader { + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + visible: displayedApp === null + icon: "apps" + title: qsTr("Launcher Applications") + } + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Appearance.padding.large * 2 + visible: displayedApp !== null + implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) + implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight + + ColumnLayout { + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + IconImage { + id: appIconImage + Layout.alignment: Qt.AlignHCenter + implicitSize: Appearance.font.size.extraLarge * 3 * 2 + source: { + const app = appDetailsLayout.displayedApp; + if (!app) return "image-missing"; + const entry = app.entry; + if (entry && entry.icon) { + return Quickshell.iconPath(entry.icon, "image-missing"); + } + return "image-missing"; + } + } + + StyledText { + id: appTitleText + Layout.alignment: Qt.AlignHCenter + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" + font.pointSize: Appearance.font.size.large + font.bold: true + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Appearance.spacing.large + Layout.leftMargin: Appearance.padding.large * 2 + Layout.rightMargin: Appearance.padding.large * 2 + + StyledFlickable { + id: detailsFlickable + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: debugLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: parent + } + + ColumnLayout { + id: debugLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + SwitchRow { + Layout.topMargin: Appearance.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Hide from launcher") + checked: root.hideFromLauncherChecked + enabled: appDetailsLayout.displayedApp !== null + onToggled: checked => { + root.hideFromLauncherChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + if (checked) { + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); + } + } else { + const index = hiddenApps.indexOf(appId); + if (index !== -1) { + hiddenApps.splice(index, 1); + } + } + Config.launcher.hiddenApps = hiddenApps; + Config.save(); + } + } + } + + } + } + } + } + } +} diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml new file mode 100644 index 0000000..161221e --- /dev/null +++ b/modules/controlcenter/launcher/Settings.qml @@ -0,0 +1,218 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +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 + + required property Session session + + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "apps" + title: qsTr("Launcher Settings") + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("General") + description: qsTr("General launcher settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("Enabled") + checked: Config.launcher.enabled + toggle.onToggled: { + Config.launcher.enabled = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Show on hover") + checked: Config.launcher.showOnHover + toggle.onToggled: { + Config.launcher.showOnHover = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Vim keybinds") + checked: Config.launcher.vimKeybinds + toggle.onToggled: { + Config.launcher.vimKeybinds = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Enable dangerous actions") + checked: Config.launcher.enableDangerousActions + toggle.onToggled: { + Config.launcher.enableDangerousActions = checked; + Config.save(); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Display") + description: qsTr("Display and appearance settings") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Max shown items") + value: qsTr("%1").arg(Config.launcher.maxShown) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Max wallpapers") + value: qsTr("%1").arg(Config.launcher.maxWallpapers) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Drag threshold") + value: qsTr("%1 px").arg(Config.launcher.dragThreshold) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Prefixes") + description: qsTr("Command prefix settings") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Special prefix") + value: Config.launcher.specialPrefix || qsTr("None") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Action prefix") + value: Config.launcher.actionPrefix || qsTr("None") + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Fuzzy search") + description: qsTr("Fuzzy search settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("Apps") + checked: Config.launcher.useFuzzy.apps + toggle.onToggled: { + Config.launcher.useFuzzy.apps = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Actions") + checked: Config.launcher.useFuzzy.actions + toggle.onToggled: { + Config.launcher.useFuzzy.actions = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Schemes") + checked: Config.launcher.useFuzzy.schemes + toggle.onToggled: { + Config.launcher.useFuzzy.schemes = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Variants") + checked: Config.launcher.useFuzzy.variants + toggle.onToggled: { + Config.launcher.useFuzzy.variants = checked; + Config.save(); + } + } + + ToggleRow { + label: qsTr("Wallpapers") + checked: Config.launcher.useFuzzy.wallpapers + toggle.onToggled: { + Config.launcher.useFuzzy.wallpapers = checked; + Config.save(); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Sizes") + description: qsTr("Size settings for launcher items") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Item width") + value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Item height") + value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper width") + value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper height") + value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Hidden apps") + description: qsTr("Applications hidden from launcher") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Total hidden") + value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) + } + } +} + diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml new file mode 100644 index 0000000..1cd6c0a --- /dev/null +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -0,0 +1,118 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +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 + + required property Session session + readonly property var ethernetDevice: root.session.ethernet.active + + device: ethernetDevice + + Component.onCompleted: { + if (ethernetDevice && ethernetDevice.interface) { + Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); + } + } + + onEthernetDeviceChanged: { + if (ethernetDevice && ethernetDevice.interface) { + Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); + } else { + Nmcli.ethernetDeviceDetails = null; + } + } + + headerComponent: Component { + ConnectionHeader { + icon: "cable" + title: root.ethernetDevice?.interface ?? qsTr("Unknown") + } + } + + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this device") + } + + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.ethernetDevice?.connected ?? false + toggle.onToggled: { + if (checked) { + Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {}); + } else { + if (root.ethernetDevice?.connection) { + Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {}); + } + } + } + } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Device properties") + description: qsTr("Additional information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Interface") + value: root.ethernetDevice?.interface ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Connection") + value: root.ethernetDevice?.connection || qsTr("Not connected") + } + + PropertyRow { + showTopMargin: true + label: qsTr("State") + value: root.ethernetDevice?.state ?? qsTr("Unknown") + } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } + + SectionContainer { + ConnectionInfoSection { + deviceDetails: Nmcli.ethernetDeviceDetails + } + } + } + } + ] +} diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml new file mode 100644 index 0000000..4f4dc8a --- /dev/null +++ b/modules/controlcenter/network/EthernetList.qml @@ -0,0 +1,177 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +DeviceList { + id: root + + required property Session session + + title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + description: qsTr("All available ethernet devices") + activeItem: session.ethernet.active + + model: Nmcli.ethernetDevices + + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + + onClicked: { + if (root.session.ethernet.active) + root.session.ethernet.active = null; + else { + root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null; + } + } + } + } + } + + delegate: Component { + StyledRect { + id: ethernetItem + + required property var modelData + readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface + + width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + + function onClicked(): void { + root.session.ethernet.active = modelData; + } + } + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + StyledRect { + anchors.fill: parent + radius: parent.radius + color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + + Behavior on fill { + Anim {} + } + } + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: modelData.interface || qsTr("Unknown") + elide: Text.ElideRight + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + elide: Text.ElideRight + } + } + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + 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, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + animate: true + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + } + } + + onItemSelected: function(item) { + session.ethernet.active = item; + } +} diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml new file mode 100644 index 0000000..126535a --- /dev/null +++ b/modules/controlcenter/network/EthernetPane.qml @@ -0,0 +1,50 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick + +SplitPaneWithDetails { + id: root + + required property Session session + + anchors.fill: parent + + activeItem: session.ethernet.active + paneIdGenerator: function(item) { + return item ? (item.interface || "") : ""; + } + + leftContent: Component { + EthernetList { + session: root.session + } + } + + rightDetailsComponent: Component { + EthernetDetails { + session: root.session + } + } + + rightSettingsComponent: Component { + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true + + EthernetSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } +} diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml new file mode 100644 index 0000000..f0f66b4 --- /dev/null +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +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 + + required property Session session + + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "cable" + title: qsTr("Ethernet settings") + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Ethernet devices") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Available ethernet devices") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: ethernetInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Total devices") + } + + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Connected devices") + } + + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } +}
\ No newline at end of file diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml new file mode 100644 index 0000000..22e07cb --- /dev/null +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -0,0 +1,98 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +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 + + required property Session session + + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "router" + title: qsTr("Network Settings") + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Ethernet") + description: qsTr("Ethernet device information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Total devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.length) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Connected devices") + value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Wireless") + description: qsTr("WiFi network settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("WiFi enabled") + checked: Nmcli.wifiEnabled + toggle.onToggled: { + Nmcli.enableWifi(checked); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Current connection") + description: qsTr("Active network connection information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Network") + value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Signal strength") + value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Security") + value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + visible: Nmcli.active !== null + label: qsTr("Frequency") + value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") + } + } +} + diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml new file mode 100644 index 0000000..b430cce --- /dev/null +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -0,0 +1,305 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." +import qs.components +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 + + required property Session session + + anchors.fill: parent + + SplitPaneLayout { + id: splitLayout + + anchors.fill: parent + + leftContent: Component { + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + ColumnLayout { + id: leftContent + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Network") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Toggle WiFi") + + onClicked: { + Nmcli.toggleWifi(null); + } + } + + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Scan for networks") + + onClicked: { + Nmcli.rescanWifi(); + } + } + + ToggleButton { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Network settings") + + onClicked: { + if (root.session.ethernet.active || root.session.network.active) { + root.session.ethernet.active = null; + root.session.network.active = null; + } else { + if (Nmcli.ethernetDevices.length > 0) { + root.session.ethernet.active = Nmcli.ethernetDevices[0]; + } else if (Nmcli.networks.length > 0) { + root.session.network.active = Nmcli.networks[0]; + } + } + } + } + } + + CollapsibleSection { + id: ethernetListSection + + Layout.fillWidth: true + title: qsTr("Ethernet") + expanded: true + + Loader { + Layout.fillWidth: true + sourceComponent: Component { + EthernetList { + session: root.session + showHeader: false + } + } + } + } + + CollapsibleSection { + id: wirelessListSection + + Layout.fillWidth: true + title: qsTr("Wireless") + expanded: true + + Loader { + Layout.fillWidth: true + sourceComponent: Component { + WirelessList { + session: root.session + showHeader: false + } + } + } + } + } + } + } + + rightContent: Component { + Item { + id: rightPaneItem + + property var ethernetPane: root.session.ethernet.active + property var wirelessPane: root.session.network.active + property var pane: ethernetPane || wirelessPane + property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings") + property Component targetComponent: settingsComponent + property Component nextComponent: settingsComponent + + function getComponentForPane() { + if (ethernetPane) return ethernetDetailsComponent; + if (wirelessPane) return wirelessDetailsComponent; + return settingsComponent; + } + + Component.onCompleted: { + targetComponent = getComponentForPane(); + nextComponent = targetComponent; + } + + Connections { + target: root.session.ethernet + function onActiveChanged() { + // Clear wireless when ethernet is selected + if (root.session.ethernet.active && root.session.network.active) { + root.session.network.active = null; + return; // Let the network.onActiveChanged handle the update + } + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + // paneId will automatically update via property binding + } + } + + Connections { + target: root.session.network + function onActiveChanged() { + // Clear ethernet when wireless is selected + if (root.session.network.active && root.session.ethernet.active) { + root.session.ethernet.active = null; + return; // Let the ethernet.onActiveChanged handle the update + } + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + // paneId will automatically update via property binding + } + } + + Loader { + id: rightLoader + + anchors.fill: parent + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + clip: false + + asynchronous: true + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + } + } + } + + Component { + id: settingsComponent + + StyledFlickable { + id: settingsFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } + + NetworkSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session + } + } + } + + Component { + id: ethernetDetailsComponent + + StyledFlickable { + id: ethernetFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: ethernetDetailsInner.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: ethernetFlickable + } + + EthernetDetails { + id: ethernetDetailsInner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session + } + } + } + + Component { + id: wirelessDetailsComponent + + StyledFlickable { + id: wirelessFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: wirelessDetailsInner.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: wirelessFlickable + } + + WirelessDetails { + id: wirelessDetailsInner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + session: root.session + } + } + } + + WirelessPasswordDialog { + anchors.fill: parent + session: root.session + z: 1000 + } +} diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml new file mode 100644 index 0000000..47d42c2 --- /dev/null +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -0,0 +1,212 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." +import qs.components +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 + + 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); + if (isActive) { + Nmcli.getWirelessDeviceDetails(""); + } else { + Nmcli.wirelessDeviceDetails = null; + } + } else { + Nmcli.wirelessDeviceDetails = null; + } + } + + headerComponent: Component { + ConnectionHeader { + icon: root.network?.isSecure ? "lock" : "wifi" + title: root.network?.ssid ?? qsTr("Unknown") + } + } + + sections: [ + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection status") + description: qsTr("Connection settings for this network") + } + + SectionContainer { + ToggleRow { + label: qsTr("Connected") + checked: root.network?.active ?? false + toggle.onToggled: { + if (checked) { + NetworkConnection.handleConnect(root.network, root.session, null); + } else { + Nmcli.disconnectFromNetwork(); + } + } + } + + TextButton { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + visible: { + if (!root.network || !root.network.ssid) { + return false; + } + return Nmcli.hasSavedProfile(root.network.ssid); + } + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Forget Network") + + onClicked: { + if (root.network && root.network.ssid) { + if (root.network.active) { + Nmcli.disconnectFromNetwork(); + } + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Network properties") + description: qsTr("Additional information") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("SSID") + value: root.network?.ssid ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("BSSID") + value: root.network?.bssid ?? qsTr("Unknown") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") + } + } + } + }, + Component { + ColumnLayout { + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Connection information") + description: qsTr("Network connection details") + } + + SectionContainer { + ConnectionInfoSection { + deviceDetails: Nmcli.wirelessDeviceDetails + } + } + } + } + ] +} diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml new file mode 100644 index 0000000..8159291 --- /dev/null +++ b/modules/controlcenter/network/WirelessList.qml @@ -0,0 +1,226 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." +import qs.components +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 + + title: qsTr("Networks (%1)").arg(Nmcli.networks.length) + description: qsTr("All available WiFi networks") + activeItem: session.network.active + + titleSuffix: Component { + StyledText { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } + } + + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + if (a.active !== b.active) + return b.active - a.active; + return b.strength - a.strength; + }) + } + + headerComponent: Component { + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + + onClicked: { + Nmcli.toggleWifi(null); + } + } + + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + + onClicked: { + Nmcli.rescanWifi(); + } + } + + ToggleButton { + toggled: !root.session.network.active + icon: "settings" + accent: "Primary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + + onClicked: { + if (root.session.network.active) + root.session.network.active = null; + else { + root.session.network.active = root.view.model.get(0)?.modelData ?? null; + } + } + } + } + } + + delegate: Component { + StyledRect { + required property var modelData + + width: ListView.view ? ListView.view.width : undefined + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + + StateLayer { + function onClicked(): void { + root.session.network.active = modelData; + if (modelData && modelData.ssid) { + root.checkSavedProfileForNetwork(modelData.ssid); + } + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) + font.pointSize: Appearance.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.ssid || qsTr("Unknown") + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: { + if (modelData.active) return qsTr("Connected"); + if (modelData.isSecure && modelData.security && modelData.security.length > 0) { + return modelData.security; + } + if (modelData.isSecure) return qsTr("Secured"); + return qsTr("Open"); + } + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.active ? 500 : 400 + elide: Text.ElideRight + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + + StateLayer { + function onClicked(): void { + if (modelData.active) { + Nmcli.disconnectFromNetwork(); + } else { + NetworkConnection.handleConnect(modelData, root.session, null); + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + + onItemSelected: function(item) { + session.network.active = item; + if (item && item.ssid) { + 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 new file mode 100644 index 0000000..109d416 --- /dev/null +++ b/modules/controlcenter/network/WirelessPane.qml @@ -0,0 +1,57 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.containers +import qs.config +import Quickshell.Widgets +import QtQuick + +SplitPaneWithDetails { + id: root + + required property Session session + + anchors.fill: parent + + activeItem: session.network.active + paneIdGenerator: function(item) { + return item ? (item.ssid || item.bssid || "") : ""; + } + + leftContent: Component { + WirelessList { + session: root.session + } + } + + rightDetailsComponent: Component { + WirelessDetails { + session: root.session + } + } + + rightSettingsComponent: Component { + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true + + WirelessSettings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + overlayComponent: Component { + WirelessPasswordDialog { + anchors.fill: parent + session: root.session + } + } +} diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml new file mode 100644 index 0000000..7c046af --- /dev/null +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -0,0 +1,512 @@ +pragma ComponentBehavior: Bound + +import ".." +import "." +import qs.components +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 + + required property Session session + + readonly property var network: { + if (session.network.pendingNetwork) { + return session.network.pendingNetwork; + } + if (session.network.active) { + return session.network.active; + } + return null; + } + + property bool isClosing: false + visible: session.network.showPasswordDialog || isClosing + enabled: session.network.showPasswordDialog && !isClosing + focus: enabled + + Keys.onEscapePressed: { + closeDialog(); + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 + + Behavior on opacity { + Anim {} + } + + MouseArea { + anchors.fill: parent + onClicked: closeDialog() + } + } + + StyledRect { + id: dialog + + anchors.centerIn: parent + + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 + scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + ParallelAnimation { + running: root.isClosing + onFinished: { + if (root.isClosing) { + root.session.network.showPasswordDialog = false; + root.isClosing = false; + } + } + + Anim { + target: dialog + property: "opacity" + to: 0 + } + Anim { + target: dialog + property: "scale" + to: 0.7 + } + } + + Keys.onEscapePressed: closeDialog() + + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.small + visible: connectButton.connecting || connectButton.hasError + text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } + if (connectButton.connecting) { + return qsTr("Connecting..."); + } + return ""; + } + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap + Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + } + + Item { + id: passwordContainer + 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 (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + if (connectButton.enabled) { + connectButton.clicked(); + } + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + passwordBuffer = ""; + } else { + passwordBuffer = passwordBuffer.slice(0, -1); + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { + passwordBuffer += event.text; + event.accepted = true; + } + } + + property string passwordBuffer: "" + + Connections { + target: root.session.network + function onShowPasswordDialogChanged(): void { + if (root.session.network.showPasswordDialog) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; + }); + } + } + } + + Connections { + target: root + function onVisibleChanged(): void { + if (root.visible) { + Qt.callLater(() => { + passwordContainer.forceActiveFocus(); + }); + } + } + } + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.visible ? Colours.palette.m3outline : "transparent"; + } + + Behavior on border.color { + CAnim {} + } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } + } + + StyledText { + id: placeholder + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + font.family: Appearance.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + ListView { + id: charList + + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + + anchors.centerIn: parent + implicitWidth: fullWidth + implicitHeight: Appearance.font.size.normal + + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 + interactive: false + + model: ScriptModel { + values: passwordContainer.passwordBuffer.split("") + } + + delegate: StyledRect { + id: ch + + implicitWidth: implicitHeight + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface + radius: Appearance.rounding.small / 2 + + opacity: 0 + scale: 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: true + } + ParallelAnimation { + Anim { + target: ch + property: "opacity" + to: 0 + } + Anim { + target: ch + property: "scale" + to: 0.5 + } + } + PropertyAction { + target: ch + property: "ListView.delayRemove" + value: false + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + id: cancelButton + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") + + onClicked: root.closeDialog() + } + + TextButton { + id: connectButton + + property bool connecting: false + property bool hasError: false + + Layout.fillWidth: true + Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary + text: qsTr("Connect") + enabled: passwordContainer.passwordBuffer.length > 0 && !connecting + + onClicked: { + if (!root.network || connecting) { + return; + } + + const password = passwordContainer.passwordBuffer; + if (!password || password.length === 0) { + return; + } + + hasError = false; + connecting = true; + enabled = false; + text = qsTr("Connecting..."); + + NetworkConnection.connectWithPassword(root.network, password, result => { + if (result && result.success) { + } else if (result && result.needsPassword) { + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + }); + + connectionMonitor.start(); + } + } + } + } + } + + 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); + } + } + } + } + + Timer { + id: connectionMonitor + interval: 1000 + repeat: true + triggeredOnStart: false + property int repeatCount: 0 + + onTriggered: { + repeatCount++; + checkConnectionStatus(); + } + + onRunningChanged: { + if (!running) { + repeatCount = 0; + } + } + } + + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + if (root.visible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + } + } + + Connections { + target: Nmcli + function onActiveChanged() { + if (root.visible) { + checkConnectionStatus(); + } + } + function onConnectionFailed(ssid: string) { + if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + Nmcli.forgetNetwork(ssid); + } + } + } + + 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 new file mode 100644 index 0000000..f87fe39 --- /dev/null +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -0,0 +1,73 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +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 + + required property Session session + + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "wifi" + title: qsTr("Network settings") + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("WiFi status") + description: qsTr("General WiFi settings") + } + + SectionContainer { + ToggleRow { + label: qsTr("WiFi enabled") + checked: Nmcli.wifiEnabled + toggle.onToggled: { + Nmcli.enableWifi(checked); + } + } + } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Network information") + description: qsTr("Current network connection") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Connected network") + value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Signal strength") + value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Security") + value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Frequency") + value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") + } + } +}
\ No newline at end of file diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml new file mode 100644 index 0000000..00497ce --- /dev/null +++ b/modules/controlcenter/state/BluetoothState.qml @@ -0,0 +1,13 @@ +import Quickshell.Bluetooth +import QtQuick + +QtObject { + id: root + + property BluetoothDevice active: null + property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter + property bool editingAdapterName: false + property bool fabMenuOpen: false + property bool editingDeviceName: false +} + diff --git a/modules/controlcenter/state/EthernetState.qml b/modules/controlcenter/state/EthernetState.qml new file mode 100644 index 0000000..c5da7aa --- /dev/null +++ b/modules/controlcenter/state/EthernetState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + id: root + + property var active: null +} + diff --git a/modules/controlcenter/state/LauncherState.qml b/modules/controlcenter/state/LauncherState.qml new file mode 100644 index 0000000..c5da7aa --- /dev/null +++ b/modules/controlcenter/state/LauncherState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + id: root + + property var active: null +} + diff --git a/modules/controlcenter/state/NetworkState.qml b/modules/controlcenter/state/NetworkState.qml new file mode 100644 index 0000000..da13e65 --- /dev/null +++ b/modules/controlcenter/state/NetworkState.qml @@ -0,0 +1,10 @@ +import QtQuick + +QtObject { + id: root + + property var active: null + property bool showPasswordDialog: false + property var pendingNetwork: null +} + diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml new file mode 100644 index 0000000..bf3a97f --- /dev/null +++ b/modules/controlcenter/taskbar/ConnectedButtonGroup.qml @@ -0,0 +1,109 @@ +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 + + property var options: [] // Array of {label: string, propertyName: string, onToggled: function} + property var rootItem: null // The root item that contains the properties we want to bind to + property string title: "" // Optional title text + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + clip: true + + Behavior on implicitHeight { + Anim {} + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + visible: root.title !== "" + text: root.title + font.pointSize: Appearance.font.size.normal + } + + RowLayout { + id: buttonRow + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small + + Repeater { + id: repeater + model: root.options + + delegate: TextButton { + id: button + required property int index + required property var modelData + + Layout.fillWidth: true + text: modelData.label + + property bool _checked: false + + checked: _checked + toggle: false + type: TextButton.Tonal + + // Create binding in Component.onCompleted + Component.onCompleted: { + if (root.rootItem && modelData.propertyName) { + const propName = modelData.propertyName; + const rootItem = root.rootItem; + _checked = Qt.binding(function() { + return rootItem[propName] ?? false; + }); + } + } + + // Match utilities Toggles radius styling + // Each button has full rounding (not connected) since they have spacing + radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + + // Match utilities Toggles inactive color + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Adjust width similar to utilities toggles + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) + + onClicked: { + if (modelData.onToggled && root.rootItem && modelData.propertyName) { + const currentValue = root.rootItem[modelData.propertyName] ?? false; + modelData.onToggled(!currentValue); + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + } + } + } +} + diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml new file mode 100644 index 0000000..5d51c8c --- /dev/null +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -0,0 +1,637 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +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 + + required property Session session + + property bool clockShowIcon: Config.bar.clock.showIcon ?? true + property bool persistent: Config.bar.persistent ?? true + property bool showOnHover: Config.bar.showOnHover ?? true + property int dragThreshold: Config.bar.dragThreshold ?? 20 + property bool showAudio: Config.bar.status.showAudio ?? true + property bool showMicrophone: Config.bar.status.showMicrophone ?? true + property bool showKbLayout: Config.bar.status.showKbLayout ?? false + property bool showNetwork: Config.bar.status.showNetwork ?? true + property bool showBluetooth: Config.bar.status.showBluetooth ?? true + property bool showBattery: Config.bar.status.showBattery ?? true + property bool showLockStatus: Config.bar.status.showLockStatus ?? true + property bool trayBackground: Config.bar.tray.background ?? false + property bool trayCompact: Config.bar.tray.compact ?? false + property bool trayRecolour: Config.bar.tray.recolour ?? false + property int workspacesShown: Config.bar.workspaces.shown ?? 5 + property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true + property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false + property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false + property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true + property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true + property bool scrollVolume: Config.bar.scrollActions.volume ?? true + property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true + property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true + property bool popoutTray: Config.bar.popouts.tray ?? true + property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true + + 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.clock.showIcon = root.clockShowIcon; + Config.bar.persistent = root.persistent; + Config.bar.showOnHover = root.showOnHover; + Config.bar.dragThreshold = root.dragThreshold; + Config.bar.status.showAudio = root.showAudio; + Config.bar.status.showMicrophone = root.showMicrophone; + Config.bar.status.showKbLayout = root.showKbLayout; + Config.bar.status.showNetwork = root.showNetwork; + Config.bar.status.showBluetooth = root.showBluetooth; + Config.bar.status.showBattery = root.showBattery; + Config.bar.status.showLockStatus = root.showLockStatus; + Config.bar.tray.background = root.trayBackground; + Config.bar.tray.compact = root.trayCompact; + Config.bar.tray.recolour = root.trayRecolour; + Config.bar.workspaces.shown = root.workspacesShown; + Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; + Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; + Config.bar.workspaces.showWindows = root.workspacesShowWindows; + Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + Config.bar.scrollActions.workspaces = root.scrollWorkspaces; + Config.bar.scrollActions.volume = root.scrollVolume; + Config.bar.scrollActions.brightness = root.scrollBrightness; + Config.bar.popouts.activeWindow = root.popoutActiveWindow; + Config.bar.popouts.tray = root.popoutTray; + Config.bar.popouts.statusIcons = root.popoutStatusIcons; + + const entries = []; + for (let i = 0; i < entriesModel.count; i++) { + const entry = entriesModel.get(i); + let enabled = entry.enabled; + if (entryIndex !== undefined && i === entryIndex) { + enabled = entryEnabled; + } + entries.push({ + id: entry.id, + enabled: enabled + }); + } + Config.bar.entries = entries; + Config.save(); + } + + ListModel { + id: entriesModel + } + + ClippingRectangle { + id: taskbarClippingRect + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal + + radius: taskbarBorder.innerRadius + color: "transparent" + + Loader { + id: taskbarLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + asynchronous: true + sourceComponent: taskbarContentComponent + } + } + + InnerBorder { + id: taskbarBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal + } + + Component { + id: taskbarContentComponent + + StyledFlickable { + id: sidebarFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sidebarFlickable + } + + ColumnLayout { + id: sidebarLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Taskbar") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Status Icons") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Speakers"), + propertyName: "showAudio", + onToggled: function(checked) { + root.showAudio = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Microphone"), + propertyName: "showMicrophone", + onToggled: function(checked) { + root.showMicrophone = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Keyboard"), + propertyName: "showKbLayout", + onToggled: function(checked) { + root.showKbLayout = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function(checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Bluetooth"), + propertyName: "showBluetooth", + onToggled: function(checked) { + root.showBluetooth = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function(checked) { + root.showBattery = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Capslock"), + propertyName: "showLockStatus", + onToggled: function(checked) { + root.showLockStatus = checked; + root.saveConfig(); + } + } + ] + } + } + + RowLayout { + id: mainRowLayout + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + ColumnLayout { + id: leftColumnLayout + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Workspaces") + font.pointSize: Appearance.font.size.normal + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesShownRow + 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: qsTr("Shown") + } + + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: value => { + root.workspacesShown = value; + root.saveConfig(); + } + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesActiveIndicatorRow + 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: qsTr("Active indicator") + } + + StyledSwitch { + checked: root.workspacesActiveIndicator + onToggled: { + root.workspacesActiveIndicator = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesOccupiedBgRow + 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: qsTr("Occupied background") + } + + StyledSwitch { + checked: root.workspacesOccupiedBg + onToggled: { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesShowWindowsRow + 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: qsTr("Show windows") + } + + StyledSwitch { + checked: root.workspacesShowWindows + onToggled: { + root.workspacesShowWindows = checked; + root.saveConfig(); + } + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesPerMonitorRow + 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: qsTr("Per monitor workspaces") + } + + StyledSwitch { + checked: root.workspacesPerMonitor + onToggled: { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } + } + } + } + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Scroll Actions") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Workspaces"), + propertyName: "scrollWorkspaces", + onToggled: function(checked) { + root.scrollWorkspaces = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Volume"), + propertyName: "scrollVolume", + onToggled: function(checked) { + root.scrollVolume = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Brightness"), + propertyName: "scrollBrightness", + onToggled: function(checked) { + root.scrollBrightness = checked; + root.saveConfig(); + } + } + ] + } + } + } + + ColumnLayout { + id: middleColumnLayout + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Clock") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; + root.saveConfig(); + } + } + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Bar Behavior") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Drag threshold") + value: root.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + root.dragThreshold = Math.round(newValue); + root.saveConfig(); + } + } + } + } + } + + ColumnLayout { + id: rightColumnLayout + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Popouts") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Active window") + checked: root.popoutActiveWindow + onToggled: checked => { + root.popoutActiveWindow = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Tray") + checked: root.popoutTray + onToggled: checked => { + root.popoutTray = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Status icons") + checked: root.popoutStatusIcons + onToggled: checked => { + root.popoutStatusIcons = checked; + root.saveConfig(); + } + } + } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Tray Settings") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + + options: [ + { + label: qsTr("Background"), + propertyName: "trayBackground", + onToggled: function(checked) { + root.trayBackground = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Compact"), + propertyName: "trayCompact", + onToggled: function(checked) { + root.trayCompact = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Recolour"), + propertyName: "trayRecolour", + onToggled: function(checked) { + root.trayRecolour = checked; + root.saveConfig(); + } + } + ] + } + } + } + } + + } + } + } +} |