diff --git a/Components/CustomSplitButton.qml b/Components/CustomSplitButton.qml index becabf5..6f9ad95 100644 --- a/Components/CustomSplitButton.qml +++ b/Components/CustomSplitButton.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import qs.Config +import qs.Helpers Row { id: root @@ -29,8 +30,29 @@ Row { property int type: CustomSplitButton.Filled property real verticalPadding: Appearance.padding.smaller + function closeDropdown(): void { + SettingsDropdowns.close(menu); + } + + function openDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.open(menu, root); + } + + function toggleDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.toggle(menu, root); + } + spacing: Math.floor(Appearance.spacing.small / 2) + onExpandedChanged: { + if (!expanded) + SettingsDropdowns.forget(menu); + } + CustomRect { bottomRightRadius: Appearance.rounding.small / 2 color: root.disabled ? root.disabledColor : root.color @@ -109,7 +131,7 @@ Row { id: expandStateLayer function onClicked(): void { - root.expanded = !root.expanded; + root.toggleDropdown(); } color: root.textColor diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml index 5b4fe79..a8295e9 100644 --- a/Components/CustomSplitButtonRow.qml +++ b/Components/CustomSplitButtonRow.qml @@ -47,9 +47,10 @@ Item { menu.onItemSelected: item => { root.selected(item); + splitButton.closeDropdown(); } stateLayer.onClicked: { - splitButton.expanded = !splitButton.expanded; + splitButton.toggleDropdown(); } } } diff --git a/Components/PathMenu.qml b/Components/PathMenu.qml new file mode 100644 index 0000000..0e2d10d --- /dev/null +++ b/Components/PathMenu.qml @@ -0,0 +1,76 @@ +import QtQuick + +Path { + id: root + + required property real viewHeight + required property real viewWidth + + startX: root.viewWidth / 2 + startY: 0 + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (1 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (2 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (3 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 1.00 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (4 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (5 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight + } + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } +} diff --git a/Components/PathViewMenu.qml b/Components/PathViewMenu.qml new file mode 100644 index 0000000..898dd4b --- /dev/null +++ b/Components/PathViewMenu.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Effects +import qs.Config + +Elevation { + id: root + + required property int currentIndex + property bool expanded + required property int from + property color insideTextColor: DynamicColors.palette.m3onPrimary + property int itemHeight + property int listHeight: 200 + property color outsideTextColor: DynamicColors.palette.m3onSurfaceVariant + readonly property var spinnerModel: root.range(root.from, root.to) + required property int to + property Item triggerItem + + signal itemSelected(item: int) + + function range(first, last) { + let out = []; + for (let i = first; i <= last; ++i) + out.push(i); + return out; + } + + implicitHeight: root.expanded ? view.implicitHeight : 0 + level: root.expanded ? 2 : 0 + radius: itemHeight / 2 + visible: implicitHeight > 0 + + Behavior on implicitHeight { + Anim { + } + } + + onExpandedChanged: { + if (!root.expanded) + root.itemSelected(view.currentIndex + 1); + } + + Component { + id: spinnerDelegate + + Item { + id: wrapper + + readonly property color delegateTextColor: wrapper.PathView.view ? wrapper.PathView.view.delegateTextColor : "white" + required property var modelData + + height: root.itemHeight + opacity: wrapper.PathView.itemOpacity + visible: wrapper.PathView.onPath + width: wrapper.PathView.view ? wrapper.PathView.view.width : 0 + z: wrapper.PathView.isCurrentItem ? 100 : Math.round(wrapper.PathView.itemScale * 100) + + CustomText { + anchors.centerIn: parent + color: wrapper.delegateTextColor + font.pointSize: Appearance.font.size.large + text: wrapper.modelData + } + } + } + + CustomClippingRect { + anchors.fill: parent + color: DynamicColors.palette.m3surfaceContainer + radius: parent.radius + + // Main visible spinner: normal/outside text color + PathView { + id: view + + property color delegateTextColor: root.outsideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: root.currentIndex - 1 + delegate: spinnerDelegate + dragMargin: width + highlightRangeMode: PathView.StrictlyEnforceRange + implicitHeight: root.listHeight + model: root.spinnerModel + pathItemCount: 7 + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + snapMode: PathView.SnapToItem + + path: PathMenu { + viewHeight: view.height + viewWidth: view.width + } + } + + // The selection rectangle itself + CustomRect { + id: selectionRect + + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + height: root.itemHeight + radius: root.itemHeight / 2 + width: parent.width + z: 2 + } + + // Hidden source: same PathView, but with the "inside selection" text color + Item { + id: selectedTextSource + + anchors.fill: parent + layer.enabled: true + visible: false + + PathView { + id: selectedTextView + + property color delegateTextColor: root.insideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: view.currentIndex + delegate: spinnerDelegate + dragMargin: view.dragMargin + highlightRangeMode: view.highlightRangeMode + implicitHeight: root.listHeight + interactive: false + model: view.model + + // Keep this PathView visually locked to the real one + offset: view.offset + pathItemCount: view.pathItemCount + preferredHighlightBegin: view.preferredHighlightBegin + preferredHighlightEnd: view.preferredHighlightEnd + snapMode: view.snapMode + + path: PathMenu { + viewHeight: selectedTextView.height + viewWidth: selectedTextView.width + } + } + } + + // Mask matching the selection rectangle + Item { + id: selectionMask + + anchors.fill: parent + layer.enabled: true + visible: false + + CustomRect { + color: "white" + height: selectionRect.height + radius: selectionRect.radius + width: selectionRect.width + x: selectionRect.x + y: selectionRect.y + } + } + + // Only show the "inside selection" text where the mask exists + MultiEffect { + anchors.fill: selectedTextSource + maskEnabled: true + maskInverted: false + maskSource: selectionMask + source: selectedTextSource + z: 3 + } + } +} diff --git a/Helpers/SettingsDropdowns.qml b/Helpers/SettingsDropdowns.qml new file mode 100644 index 0000000..4a4ac27 --- /dev/null +++ b/Helpers/SettingsDropdowns.qml @@ -0,0 +1,60 @@ +pragma Singleton +import QtQuick + +QtObject { + id: root + + property Item activeMenu: null + property Item activeTrigger: null + + function close(menu) { + if (!menu) + return; + + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + + menu.expanded = false; + } + + function closeActive() { + if (activeMenu) + activeMenu.expanded = false; + + activeMenu = null; + activeTrigger = null; + } + + function forget(menu) { + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + } + + function hit(item, scenePos) { + if (!item || !item.visible) + return false; + + const p = item.mapFromItem(null, scenePos.x, scenePos.y); + return item.contains(p); + } + + function open(menu, trigger) { + if (activeMenu && activeMenu !== menu) + activeMenu.expanded = false; + + activeMenu = menu; + activeTrigger = trigger || null; + menu.expanded = true; + } + + function toggle(menu, trigger) { + if (activeMenu === menu && menu.expanded) + close(menu); + else + open(menu, trigger); + } +} diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index c4401bc..bd4da4e 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -94,11 +94,13 @@ Item { CustomListView { id: clayout - anchors.centerIn: parent - contentHeight: contentItem.childrenRect.height + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Appearance.padding.smaller + anchors.top: parent.top + boundsBehavior: Flickable.StopAtBounds contentWidth: contentItem.childrenRect.width highlightFollowsCurrentItem: false - implicitHeight: contentItem.childrenRect.height implicitWidth: contentItem.childrenRect.width model: listModel spacing: 5 @@ -109,7 +111,7 @@ Item { color: DynamicColors.palette.m3primary implicitHeight: clayout.currentItem?.implicitHeight ?? 0 implicitWidth: clayout.width - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller y: clayout.currentItem?.y ?? 0 Behavior on y { @@ -131,7 +133,7 @@ Item { implicitHeight: 42 implicitWidth: 200 - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller RowLayout { id: layout diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index b7979ad..1c6d237 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -5,7 +5,6 @@ import QtQuick.Layouts import qs.Components import qs.Modules as Modules import qs.Modules.Settings.Controls -import qs.Modules.Settings.Categories.Appearance import qs.Config import qs.Helpers @@ -14,13 +13,33 @@ CustomFlickable { contentHeight: clayout.implicitHeight + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + ColumnLayout { id: clayout anchors.left: parent.left anchors.right: parent.right - CustomClippingRect { + CustomRect { Layout.fillWidth: true Layout.preferredHeight: colorLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer @@ -33,6 +52,7 @@ CustomFlickable { anchors.margins: Appearance.padding.large anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal Settings { name: "Color" @@ -56,19 +76,11 @@ CustomFlickable { Separator { } - SettingInput { - name: "Schedule dark mode start" + SettingSpinner { + name: "Schedule dark mode" object: Config.general.color - setting: "scheduleDarkStart" - } - - Separator { - } - - SettingInput { - name: "Schedule dark mode end" - object: Config.general.color - setting: "scheduleDarkEnd" + settings: ["scheduleDarkStart", "scheduleDarkEnd"] + z: 2 } Separator { @@ -106,22 +118,6 @@ CustomFlickable { } } } - - CustomClippingRect { - Layout.fillWidth: true - Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - Appearance.padding.smaller - - Idle { - id: idleLayout - - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - } } component Settings: CustomRect { @@ -135,9 +131,7 @@ CustomFlickable { CustomText { id: text - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + anchors.fill: parent font.bold: true font.pointSize: Appearance.font.size.large * 2 text: settingsItem.name diff --git a/Modules/Settings/Categories/General.qml b/Modules/Settings/Categories/General.qml index 3e8335c..e35999b 100644 --- a/Modules/Settings/Categories/General.qml +++ b/Modules/Settings/Categories/General.qml @@ -14,13 +14,6 @@ CustomRect { id: clayout anchors.fill: parent - - Settings { - name: "apps" - } - - Item { - } } component Settings: CustomRect { @@ -28,28 +21,17 @@ CustomRect { required property string name - implicitHeight: 42 - implicitWidth: 200 - radius: 4 + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 - RowLayout { - id: layout + CustomText { + id: text - anchors.left: parent.left - anchors.margins: Appearance.padding.smaller - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - CustomText { - id: text - - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - Layout.fillHeight: true - Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.normal - text: settingsItem.name - verticalAlignment: Text.AlignVCenter - } + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter } } } diff --git a/Modules/Settings/Categories/Lockscreen.qml b/Modules/Settings/Categories/Lockscreen.qml new file mode 100644 index 0000000..570aecc --- /dev/null +++ b/Modules/Settings/Categories/Lockscreen.qml @@ -0,0 +1,77 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Modules as Modules +import qs.Modules.Settings.Categories.Lockscreen +import qs.Config +import qs.Helpers + +CustomFlickable { + id: root + + contentHeight: clayout.implicitHeight + + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + + ColumnLayout { + id: clayout + + anchors.fill: parent + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller + z: -1 + + Idle { + id: idleLayout + + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + } + } + + component Settings: CustomRect { + id: settingsItem + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + id: text + + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/Modules/Settings/Categories/Appearance/Idle.qml b/Modules/Settings/Categories/Lockscreen/Idle.qml similarity index 74% rename from Modules/Settings/Categories/Appearance/Idle.qml rename to Modules/Settings/Categories/Lockscreen/Idle.qml index f405c1d..eba0d67 100644 --- a/Modules/Settings/Categories/Appearance/Idle.qml +++ b/Modules/Settings/Categories/Lockscreen/Idle.qml @@ -36,6 +36,10 @@ ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller + Settings { + name: "Idle Monitors" + } + Repeater { model: [...Config.general.idle.timeouts] @@ -57,4 +61,23 @@ ColumnLayout { onClicked: root.addTimeoutEntry() } + + component Settings: CustomRect { + id: settingsItem + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + id: text + + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter + } + } } diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index de1c8e7..3bb23a2 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -23,13 +23,14 @@ Item { Connections { function onCurrentCategoryChanged() { stack.pop(); - if (currentCategory === "general") { + if (currentCategory === "general") stack.push(general); - } else if (currentCategory === "wallpaper") { + else if (currentCategory === "wallpaper") stack.push(background); - } else if (currentCategory === "appearance") { + else if (currentCategory === "appearance") stack.push(appearance); - } + else if (currentCategory === "lockscreen") + stack.push(lockscreen); } target: root @@ -48,7 +49,6 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.top: parent.top - implicitHeight: layout.implicitHeight implicitWidth: layout.implicitWidth Categories { @@ -100,4 +100,11 @@ Item { Cat.Appearance { } } + + Component { + id: lockscreen + + Cat.Lockscreen { + } + } } diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml index 6fcb997..2a905ca 100644 --- a/Modules/Settings/Controls/SettingList.qml +++ b/Modules/Settings/Controls/SettingList.qml @@ -229,14 +229,20 @@ Item { anchors.right: parent.right visible: root.modelData.activeAction === undefined - Item { - Layout.fillWidth: true + IconButton { + id: button + + Layout.alignment: Qt.AlignLeft + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: console.log(button.width) + // onClicked: root.addActiveActionRequested() } - CustomButton { + CustomText { + Layout.alignment: Qt.AlignLeft text: qsTr("Add active action") - - onClicked: root.addActiveActionRequested() } } } diff --git a/Modules/Settings/Controls/SettingSpinner.qml b/Modules/Settings/Controls/SettingSpinner.qml new file mode 100644 index 0000000..960e5ca --- /dev/null +++ b/Modules/Settings/Controls/SettingSpinner.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property list settings + + function commitChoice(choice: int, setting: string): void { + root.object[setting] = choice; + Config.save(); + } + + function formattedValue(setting: string): string { + const value = root.object[setting]; + + console.log(value); + if (value === null || value === undefined) + return ""; + + return String(value); + } + + function hourToAmPm(hour) { + var h = Number(hour) % 24; + var d = new Date(2000, 0, 1, h, 0, 0); + return Qt.formatTime(d, "h AP"); + } + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + Layout.alignment: Qt.AlignLeft + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(root.hourToAmPm(root.object[root.settings[0]])).arg(root.hourToAmPm(root.object[root.settings[1]])) + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[0]] + text: root.formattedValue(root.settings[0]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[0]); + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[1]] + text: root.formattedValue(root.settings[1]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[1]); + } + } + } +} diff --git a/Modules/Settings/Controls/SpinnerButton.qml b/Modules/Settings/Controls/SpinnerButton.qml new file mode 100644 index 0000000..7d5bee2 --- /dev/null +++ b/Modules/Settings/Controls/SpinnerButton.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property alias currentIndex: menu.currentIndex + property alias expanded: menu.expanded + property alias label: label + property alias menu: menu + property alias text: label.text + + color: DynamicColors.palette.m3primary + radius: Appearance.rounding.full + + CustomText { + id: label + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + font.pointSize: Appearance.font.size.large + } + + StateLayer { + function onClicked(): void { + SettingsDropdowns.toggle(menu, root); + } + } + + PathViewMenu { + id: menu + + anchors.centerIn: parent + from: 1 + implicitWidth: root.width + itemHeight: root.height + to: 24 + } +}