diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 561e8749d..e63537ca1 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -324,6 +324,9 @@ Singleton { property var screenPreferences: ({}) property var showOnLastDisplay: ({}) + property int maxSystemVolume: 100 + property int maxMediaVolume: 100 + property var barConfigs: [ { id: "default", diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index c1c2ee97e..c38120599 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -224,6 +224,9 @@ var SPEC = { screenPreferences: { def: {} }, showOnLastDisplay: { def: {} }, + maxSystemVolume: { def: 100 }, + maxMediaVolume: { def: 100 }, + barConfigs: { def: [{ id: "default", name: "Main Bar", diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index 54329e393..cbbe769a1 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -197,13 +197,30 @@ FocusScope { } Loader { - id: powerLoader + id: audioLoader anchors.fill: parent active: root.currentIndex === 10 visible: active focus: active + sourceComponent: AudioTab {} + + onActiveChanged: { + if (active && item) { + Qt.callLater(() => item.forceActiveFocus()); + } + } + } + + Loader { + id: powerLoader + + anchors.fill: parent + active: root.currentIndex === 11 + visible: active + focus: active + sourceComponent: PowerSettings {} onActiveChanged: { @@ -217,7 +234,7 @@ FocusScope { id: pluginsLoader anchors.fill: parent - active: root.currentIndex === 11 + active: root.currentIndex === 12 visible: active focus: active @@ -236,7 +253,7 @@ FocusScope { id: aboutLoader anchors.fill: parent - active: root.currentIndex === 12 + active: root.currentIndex === 13 visible: active focus: active diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index 6de863e38..7ad736423 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -64,20 +64,25 @@ Rectangle { "icon": "palette", "tabIndex": 9 }, + { + "text": I18n.tr("Audio"), + "icon": "headphones", + "tabIndex": 10 + }, { "text": I18n.tr("Power & Security"), "icon": "power", - "tabIndex": 10 + "tabIndex": 11 }, { "text": I18n.tr("Plugins"), "icon": "extension", - "tabIndex": 11 + "tabIndex": 12 }, { "text": I18n.tr("About"), "icon": "info", - "tabIndex": 12 + "tabIndex": 13 } ] readonly property var sidebarItems: allSidebarItems.filter(item => { diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index 1ab29ef9d..0f43c78d0 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -452,7 +452,7 @@ Column { let currentVolume = AudioService.sink.audio.volume * 100; let newVolume; if (delta > 0) - newVolume = Math.min(100, currentVolume + 5); + newVolume = Math.min(SettingsData.maxSystemVolume, currentVolume + 5); else newVolume = Math.max(0, currentVolume - 5); AudioService.sink.audio.muted = false; diff --git a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml index 0a81e4d3b..706e39684 100644 --- a/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/AudioOutputDetail.qml @@ -97,8 +97,9 @@ Rectangle { width: parent.width - (Theme.iconSize + Theme.spacingS * 2) enabled: AudioService.sink && AudioService.sink.audio minimum: 0 - maximum: 100 - value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0 + maximum: SettingsData.maxSystemVolume + reference: 100 + value: AudioService.sink && AudioService.sink.audio ? Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) : 0 showValue: true unit: "%" valueOverride: actualVolumePercent @@ -438,8 +439,9 @@ Rectangle { width: 100 enabled: modelData !== null minimum: 0 - maximum: 100 - value: modelData ? Math.min(100, Math.round(modelData.audio.volume * 100)) : 0 + maximum: SettingsData.maxMediaVolume + reference: 100 + value: modelData ? Math.min(SettingsData.maxMediaVolume, Math.round(modelData.audio.volume * 100)) : 0 showValue: true unit: "%" valueOverride: actualVolumePercent diff --git a/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml index 18358b33a..1b5f61e44 100644 --- a/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/AudioSliderRow.qml @@ -62,8 +62,9 @@ Row { width: parent.width - (Theme.iconSize + Theme.spacingS * 2) enabled: defaultSink !== null minimum: 0 - maximum: 100 - value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0 + maximum: SettingsData.maxSystemVolume + reference: 100 + value: defaultSink ? Math.min(SettingsData.maxSystemVolume, Math.round(defaultSink.audio.volume * 100)) : 0 showValue: true unit: "%" valueOverride: actualVolumePercent @@ -88,4 +89,4 @@ Row { } } } -} \ No newline at end of file +} diff --git a/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml index fd7944c42..af763df2c 100644 --- a/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml @@ -79,4 +79,4 @@ Row { } } } -} \ No newline at end of file +} diff --git a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml index d35bfee5b..b48147216 100644 --- a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml +++ b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml @@ -117,7 +117,7 @@ BasePill { if (!AudioService.sink?.audio) return; const currentVolume = AudioService.sink.audio.volume * 100; - const newVolume = delta > 0 ? Math.min(100, currentVolume + 5) : Math.max(0, currentVolume - 5); + const newVolume = delta > 0 ? Math.min(SettingsData.maxSystemVolume, currentVolume + 5) : Math.max(0, currentVolume - 5); AudioService.sink.audio.muted = false; AudioService.sink.audio.volume = newVolume / 100; AudioService.playVolumeChangeSoundIfEnabled(); diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 1339b15e7..fbad26628 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -54,7 +54,7 @@ Item { height: 180 x: isRightEdge ? anchorPos.x : anchorPos.x - width y: anchorPos.y - height / 2 - radius: Theme.cornerRadius * 2 + radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.width: 1 @@ -89,94 +89,67 @@ Item { shadowOpacity: 0.7 } - MouseArea { - anchors.fill: parent - anchors.margins: -12 - hoverEnabled: true - onEntered: volumeAreaEntered() - onExited: volumeAreaExited() - } - Item { anchors.fill: parent anchors.margins: Theme.spacingS - Item { - id: volumeSlider - width: parent.width * 0.5 - height: parent.height - Theme.spacingXL * 2 - anchors.top: parent.top - anchors.topMargin: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter + property int gap: Theme.spacingS - Rectangle { - width: parent.width - height: parent.height - anchors.centerIn: parent - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - radius: Theme.cornerRadius + HoverHandler { + id: hover + onHoveredChanged: { + if (hover.hovered) volumeAreaEntered() + else volumeAreaExited() } + } - Rectangle { - width: parent.width - height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - bottomLeftRadius: Theme.cornerRadius - bottomRightRadius: Theme.cornerRadius - } + DankSlider { + id: volumeSlider + + readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0 + readonly property real displayPercent: actualVolumePercent - Rectangle { - width: parent.width + 8 - height: 8 - radius: Theme.cornerRadius - y: { - const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0; - const travel = parent.height - height; - return Math.max(0, Math.min(travel, travel * (1 - ratio))); + width: parent.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + orientation: DankSlider.Vertical + minimum: 0 + maximum: SettingsData.maxSystemVolume + reference: 100 + enabled: AudioService.sink && AudioService.sink.audio + showValue: true + unit: "%" + thumbOutlineColor: Theme.surfaceContainer + valueOverride: displayPercent + alwaysShowValue: SettingsData.osdAlwaysShowValue + + Component.onCompleted: { + if (AudioService.sink && AudioService.sink.audio) { + value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) } - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - border.width: 3 - border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0) } - MouseArea { - anchors.fill: parent - anchors.margins: -12 - enabled: volumeAvailable - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - preventStealing: true - - onEntered: volumeAreaEntered() - onExited: volumeAreaExited() - onPressed: mouse => updateVolume(mouse) - onPositionChanged: mouse => { - if (pressed) - updateVolume(mouse); - } - onClicked: mouse => updateVolume(mouse) - - function updateVolume(mouse) { - if (!volumeAvailable) - return; - const ratio = 1.0 - (mouse.y / height); - const volume = Math.max(0, Math.min(1, ratio)); - root.volumeChanged(volume); - } + onSliderValueChanged: newValue => { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.suppressOSD = true + AudioService.sink.audio.volume = newValue / 100 + AudioService.suppressOSD = false + } + } + + onContainsMouseChanged: { + setChildHovered(containsMouse || muteButton.containsMouse) } - } - StyledText { - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: Theme.spacingL - text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium + Connections { + target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null + + function onVolumeChanged() { + if (volumeSlider && !volumeSlider.pressed) { + volumeSlider.value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) + } + } + } } } } diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 0d1cd4235..4abdd7fd4 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -203,11 +203,12 @@ Item { if (!volumeAvailable) return; const current = Math.round(currentVolume * 100); - const newVolume = Math.min(100, Math.max(0, current + step)); if (usePlayerVolume) { + const newVolume = Math.min(SettingsData.maxMediaVolume, Math.max(0, current + step)); activePlayer.volume = newVolume / 100; } else if (AudioService.sink?.audio) { + const newVolume = Math.min(SettingsData.maxSystemVolume, Math.max(0, current + step)); AudioService.sink.audio.volume = newVolume / 100; } } @@ -809,11 +810,12 @@ Item { onWheel: wheelEvent => { const delta = wheelEvent.angleDelta.y; const current = (currentVolume * 100) || 0; - const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5); if (usePlayerVolume) { + const newVolume = delta > 0 ? Math.min(SettingsData.maxMediaVolume, current + 5) : Math.max(0, current - 5); activePlayer.volume = newVolume / 100; } else if (AudioService.sink?.audio) { + const newVolume = delta > 0 ? Math.min(SettingsData.maxSystemVolume, current + 5) : Math.max(0, current - 5); AudioService.sink.audio.volume = newVolume / 100; } wheelEvent.accepted = true; diff --git a/quickshell/Modules/OSD/MediaVolumeOSD.qml b/quickshell/Modules/OSD/MediaVolumeOSD.qml index 2ba4ddfad..c974929ff 100644 --- a/quickshell/Modules/OSD/MediaVolumeOSD.qml +++ b/quickshell/Modules/OSD/MediaVolumeOSD.qml @@ -105,18 +105,25 @@ DankOSD { DankSlider { id: volumeSlider + orientation: DankSlider.Horizontal width: parent.width - Theme.iconSize - parent.gap * 3 height: 40 x: parent.gap * 2 + Theme.iconSize anchors.verticalCenter: parent.verticalCenter minimum: 0 - maximum: 100 + maximum: SettingsData.maxMediaVolume + reference: 100 enabled: volumeSupported showValue: true unit: "%" thumbOutlineColor: Theme.surfaceContainer valueOverride: currentVolume alwaysShowValue: SettingsData.osdAlwaysShowValue + tooltipPlacement: [ + SettingsData.Position.Top, + SettingsData.Position.TopCenter, + SettingsData.Position.Left, + ].includes(SettingsData.osdPosition) ? DankSlider.After : DankSlider.Before Component.onCompleted: { value = currentVolume; @@ -162,111 +169,60 @@ DankOSD { anchors.centerIn: parent name: getVolumeIcon(player?.volume ?? 0) size: Theme.iconSize - color: muteButtonVert.containsMouse ? Theme.primary : Theme.surfaceText + color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText } MouseArea { - id: muteButtonVert + id: muteButton anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: toggleMute() onContainsMouseChanged: { - setChildHovered(containsMouse || vertSliderArea.containsMouse); + setChildHovered(containsMouse || volumeSlider.containsMouse); } } } - Item { - id: vertSlider + + DankSlider { + id: volumeSlider + + orientation: DankSlider.Vertical width: 12 height: parent.height - Theme.iconSize - gap * 3 - 24 - anchors.horizontalCenter: parent.horizontalCenter y: gap * 2 + Theme.iconSize + anchors.horizontalCenter: parent.horizontalCenter + minimum: 0 + maximum: SettingsData.maxMediaVolume + reference: 100 + enabled: volumeSupported + showValue: true + unit: "%" + thumbOutlineColor: Theme.surfaceContainer + valueOverride: currentVolume + alwaysShowValue: SettingsData.osdAlwaysShowValue + tooltipPlacement: SettingsData.osdPosition === SettingsData.Position.RightCenter ? DankSlider.Before : DankSlider.After - property bool dragging: false - property int value: currentVolume - - Rectangle { - id: vertTrack - width: parent.width - height: parent.height - anchors.centerIn: parent - color: Theme.outline - radius: Theme.cornerRadius - } - - Rectangle { - id: vertFill - width: parent.width - height: (vertSlider.value / 100) * parent.height - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - radius: Theme.cornerRadius + Component.onCompleted: { + value = currentVolume; } - Rectangle { - id: vertHandle - width: 24 - height: 8 - radius: Theme.cornerRadius - y: { - const ratio = vertSlider.value / 100; - const travel = parent.height - height; - return Math.max(0, Math.min(travel, travel * (1 - ratio))); - } - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - border.width: 3 - border.color: Theme.surfaceContainer + onSliderValueChanged: newValue => { + setVolume(newValue); } - MouseArea { - id: vertSliderArea - anchors.fill: parent - anchors.margins: -12 - enabled: volumeSupported - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onContainsMouseChanged: { - setChildHovered(containsMouse || muteButtonVert.containsMouse); - } - - onPressed: mouse => { - vertSlider.dragging = true; - updateVolume(mouse); - } - - onReleased: { - vertSlider.dragging = false; - } - - onPositionChanged: mouse => { - if (pressed) { - updateVolume(mouse); - } - } - - onClicked: mouse => { - updateVolume(mouse); - } - - function updateVolume(mouse) { - const ratio = 1.0 - (mouse.y / height); - const volume = Math.max(0, Math.min(100, Math.round(ratio * 100))); - setVolume(volume); - } + onContainsMouseChanged: { + setChildHovered(containsMouse || muteButton.containsMouse); } Connections { target: player function onVolumeChanged() { - if (!vertSlider.dragging) { - vertSlider.value = currentVolume; + if (volumeSlider && !volumeSlider.pressed) { + volumeSlider.value = currentVolume; } } } @@ -276,7 +232,7 @@ DankOSD { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: gap - text: vertSlider.value + "%" + text: volumeSlider.value + "%" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText visible: SettingsData.osdAlwaysShowValue diff --git a/quickshell/Modules/OSD/VolumeOSD.qml b/quickshell/Modules/OSD/VolumeOSD.qml index 548aadf5b..d316ad1f2 100644 --- a/quickshell/Modules/OSD/VolumeOSD.qml +++ b/quickshell/Modules/OSD/VolumeOSD.qml @@ -90,22 +90,29 @@ DankOSD { readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0 readonly property real displayPercent: actualVolumePercent + orientation: DankSlider.Horizontal width: parent.width - Theme.iconSize - parent.gap * 3 height: 40 x: parent.gap * 2 + Theme.iconSize anchors.verticalCenter: parent.verticalCenter minimum: 0 - maximum: 100 + maximum: SettingsData.maxSystemVolume + reference: 100 enabled: AudioService.sink && AudioService.sink.audio showValue: true unit: "%" thumbOutlineColor: Theme.surfaceContainer valueOverride: displayPercent alwaysShowValue: SettingsData.osdAlwaysShowValue + tooltipPlacement: [ + SettingsData.Position.Top, + SettingsData.Position.TopCenter, + SettingsData.Position.Left, + ].includes(SettingsData.osdPosition) ? DankSlider.After : DankSlider.Before Component.onCompleted: { if (AudioService.sink && AudioService.sink.audio) { - value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) + value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) } } @@ -127,7 +134,7 @@ DankOSD { function onVolumeChanged() { if (volumeSlider && !volumeSlider.pressed) { - volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) + volumeSlider.value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) } } } @@ -172,100 +179,53 @@ DankOSD { } } - Item { - id: vertSlider + DankSlider { + id: volumeSlider + + readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0 + readonly property real displayPercent: actualVolumePercent + + orientation: DankSlider.Vertical width: 12 height: parent.height - Theme.iconSize - gap * 3 - 24 anchors.horizontalCenter: parent.horizontalCenter y: gap * 2 + Theme.iconSize + minimum: 0 + maximum: SettingsData.maxSystemVolume + reference: 100 + enabled: AudioService.sink && AudioService.sink.audio + showValue: true + unit: "%" + thumbOutlineColor: Theme.surfaceContainer + valueOverride: displayPercent + alwaysShowValue: SettingsData.osdAlwaysShowValue + tooltipPlacement: SettingsData.osdPosition === SettingsData.Position.RightCenter ? DankSlider.Before : DankSlider.After - property bool dragging: false - property int value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0 - - Rectangle { - id: vertTrack - width: parent.width - height: parent.height - anchors.centerIn: parent - color: Theme.outline - radius: Theme.cornerRadius - } - - Rectangle { - id: vertFill - width: parent.width - height: (vertSlider.value / 100) * parent.height - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - radius: Theme.cornerRadius - } - - Rectangle { - id: vertHandle - width: 24 - height: 8 - radius: Theme.cornerRadius - y: { - const ratio = vertSlider.value / 100 - const travel = parent.height - height - return Math.max(0, Math.min(travel, travel * (1 - ratio))) + Component.onCompleted: { + if (AudioService.sink && AudioService.sink.audio) { + value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) } - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.primary - border.width: 3 - border.color: Theme.surfaceContainer } - MouseArea { - id: vertSliderArea - anchors.fill: parent - anchors.margins: -12 - enabled: AudioService.sink && AudioService.sink.audio - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onContainsMouseChanged: { - setChildHovered(containsMouse || muteButtonVert.containsMouse) - } - - onPressed: mouse => { - vertSlider.dragging = true - updateVolume(mouse) - } - - onReleased: { - vertSlider.dragging = false - } - - onPositionChanged: mouse => { - if (pressed) { - updateVolume(mouse) - } - } - - onClicked: mouse => { - updateVolume(mouse) - } + onSliderValueChanged: newValue => { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.suppressOSD = true + AudioService.sink.audio.volume = newValue / 100 + AudioService.suppressOSD = false + resetHideTimer() + } + } - function updateVolume(mouse) { - if (AudioService.sink && AudioService.sink.audio) { - const ratio = 1.0 - (mouse.y / height) - const volume = Math.max(0, Math.min(100, Math.round(ratio * 100))) - AudioService.suppressOSD = true - AudioService.sink.audio.volume = volume / 100 - AudioService.suppressOSD = false - resetHideTimer() - } - } + onContainsMouseChanged: { + setChildHovered(containsMouse || muteButtonVert.containsMouse) } Connections { target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null function onVolumeChanged() { - if (!vertSlider.dragging) { - vertSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) + if (volumeSlider && !volumeSlider.pressed) { + volumeSlider.value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) } } } @@ -275,7 +235,7 @@ DankOSD { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: gap - text: vertSlider.value + "%" + text: volumeSlider.value + "%" font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText visible: SettingsData.osdAlwaysShowValue @@ -288,7 +248,7 @@ DankOSD { if (!useVertical) { const slider = contentLoader.item.item.children[0].children[1] if (slider && slider.value !== undefined) { - slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) + slider.value = Math.min(SettingsData.maxSystemVolume, Math.round(AudioService.sink.audio.volume * 100)) } } } diff --git a/quickshell/Modules/Settings/AudioTab.qml b/quickshell/Modules/Settings/AudioTab.qml new file mode 100644 index 000000000..6b4eba7c5 --- /dev/null +++ b/quickshell/Modules/Settings/AudioTab.qml @@ -0,0 +1,241 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: dockTab + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + // Max Volume Section + StyledRect { + width: parent.width + height: maxVolumeSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: maxVolumeSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "instant_mix" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Max Volumes") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + id: maxSystemVolumeText + text: I18n.tr("Max System Volume") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + + Item { + height: parent.height + width: parent.width - maxSystemVolumeText.width - resetMaxSystemVolumeBtn.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + id: resetMaxSystemVolumeBtn + buttonSize: 24 + iconName: "refresh" + iconSize: 16 + backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + iconColor: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + onClicked: { + SettingsData.set("maxSystemVolume", 100); + maxSystemVolumeSlider.value = 100 + } + } + } + + DankSlider { + id: maxSystemVolumeSlider + width: parent.width + height: 24 + value: SettingsData.maxSystemVolume + minimum: 0 + maximum: 200 + reference: 100 + unit: "%" + showValue: true + wheelEnabled: false + thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + onSliderValueChanged: newValue => { + SettingsData.set("maxSystemVolume", newValue); + } + } + + Row { + id: maxSystemWarning + spacing: Theme.spacingS + opacity: maxSystemVolumeSlider.value > 100 ? 1 : 0 + height: maxSystemVolumeSlider.value > 100 ? maxSystemVolumeWarningText.height : 0 + width: parent.width + + DankIcon { + name: "warning" + size: Theme.iconSizeSmall + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: maxSystemVolumeWarningText + text: I18n.tr("Setting volumes above 100% could cause distortion or in rare cases damage. Do so at your own risk.") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + width: Math.min(parent.width - Theme.iconSizeSmall - Theme.spacingS, 440) + wrapMode: Text.WordWrap + } + + states: State { + name: "moved"; when: maxSystemVolumeSlider.value > 100 + PropertyChanges { + target: maxSystemWarning + opacity: maxSystemVolumeSlider.value > 100 ? 1 : 0 + height: maxSystemVolumeSlider.value > 100 ? maxSystemVolumeWarningText.height : 0 + } + } + + transitions: Transition { + NumberAnimation { properties: "height,opacity"; easing.type: Easing.OutQuad } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + + StyledText { + id: maxMediaVolumeText + text: I18n.tr("Max Media Volume") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + + Item { + height: parent.height + width: parent.width - maxMediaVolumeText.width - resetMaxMediaVolumeBtn.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + id: resetMaxMediaVolumeBtn + buttonSize: 24 + iconName: "refresh" + iconSize: 16 + backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + iconColor: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + onClicked: { + SettingsData.set("maxMediaVolume", 100); + maxMediaVolumeSlider.value = 100 + } + } + } + + DankSlider { + id: maxMediaVolumeSlider + width: parent.width + height: 24 + value: SettingsData.maxMediaVolume + minimum: 0 + maximum: 200 + reference: 100 + unit: "%" + showValue: true + wheelEnabled: false + thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + onSliderValueChanged: newValue => { + SettingsData.set("maxMediaVolume", newValue); + } + } + + Row { + spacing: Theme.spacingS + opacity: maxMediaVolumeSlider.value > 100 ? 1 : 0 + height: maxMediaVolumeSlider.value > 100 ? maxMediaVolumeWarningText.height : 0 + width: parent.width + + DankIcon { + name: "warning" + size: Theme.iconSizeSmall + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: maxMediaVolumeWarningText + text: I18n.tr("Setting volumes above 100% could cause distortion or in rare cases damage. Do so at your own risk.") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + width: Math.min(parent.width - Theme.iconSizeSmall - Theme.spacingS, 440) + wrapMode: Text.WordWrap + } + + states: State { + name: "moved"; when: maxSystemVolumeSlider.value > 100 + PropertyChanges { + target: maxSystemWarning + opacity: maxSystemVolumeSlider.value > 100 ? 1 : 0 + height: maxSystemVolumeSlider.value > 100 ? maxSystemVolumeWarningText.height : 0 + } + } + + transitions: Transition { + NumberAnimation { properties: "height,opacity"; easing.type: Easing.OutQuad } + } + } + } + } + } + } +} diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index 1e60b1dee..e6e16788e 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -486,7 +486,7 @@ Singleton { return "No audio sink available"; } - const clampedVolume = Math.max(0, Math.min(100, percentage)); + const clampedVolume = Math.max(0, Math.min(SettingsData.maxSystemVolume, percentage)); root.sink.audio.volume = clampedVolume / 100; return `Volume set to ${clampedVolume}%`; } @@ -537,7 +537,7 @@ Singleton { const currentVolume = Math.round(root.sink.audio.volume * 100); const stepValue = parseInt(step || "5"); - const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue)); + const newVolume = Math.max(0, Math.min(SettingsData.maxSystemVolume, currentVolume + stepValue)); root.sink.audio.volume = newVolume / 100; return `Volume increased to ${newVolume}%`; @@ -554,7 +554,7 @@ Singleton { const currentVolume = Math.round(root.sink.audio.volume * 100); const stepValue = parseInt(step || "5"); - const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue)); + const newVolume = Math.max(0, Math.min(SettingsData.maxSystemVolume, currentVolume - stepValue)); root.sink.audio.volume = newVolume / 100; return `Volume decreased to ${newVolume}%`; diff --git a/quickshell/Widgets/DankSlider.qml b/quickshell/Widgets/DankSlider.qml index e2a3b4603..87b0e74c3 100644 --- a/quickshell/Widgets/DankSlider.qml +++ b/quickshell/Widgets/DankSlider.qml @@ -9,6 +9,7 @@ Item { property int value: 50 property int minimum: 0 property int maximum: 100 + property var reference: null property string leftIcon: "" property string rightIcon: "" property bool enabled: true @@ -18,7 +19,7 @@ Item { property bool wheelEnabled: true property real valueOverride: -1 property bool alwaysShowValue: false - readonly property bool containsMouse: sliderMouseArea.containsMouse + readonly property bool containsMouse: loader.item ? loader.item.containsMouse : false property color thumbOutlineColor: Theme.surfaceContainer property color trackColor: enabled ? Theme.outline : Theme.outline @@ -26,10 +27,21 @@ Item { signal sliderValueChanged(int newValue) signal sliderDragFinished(int finalValue) - height: 48 + enum Orientation { Horizontal, Vertical } + property int orientation: DankSlider.Horizontal + enum TooltipPlacement { Before, After } + property int tooltipPlacement: orientation === DankSlider.Horizontal ? DankSlider.Before : DankSlider.After - function updateValueFromPosition(x) { - let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width))) + height: orientation === DankSlider.Horizontal ? 48 : parent.height + width: orientation === DankSlider.Horizontal ? parent.width : 48 + + function updateValueFromPosition(pos, sliderHandle, sliderTrack) { + let ratio + if (orientation === DankSlider.Horizontal) { + ratio = Math.max(0, Math.min(1, (pos - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width))) + } else { + ratio = 1 - Math.max(0, Math.min(1, (pos - sliderHandle.height / 2) / (sliderTrack.height - sliderHandle.height))) + } let newValue = Math.round(minimum + ratio * (maximum - minimum)) if (newValue !== value) { value = newValue @@ -37,238 +49,524 @@ Item { } } - Row { - anchors.centerIn: parent - width: parent.width - spacing: Theme.spacingM - - DankIcon { - name: slider.leftIcon - size: Theme.iconSize - color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 - anchors.verticalCenter: parent.verticalCenter - visible: slider.leftIcon.length > 0 - } - - StyledRect { - id: sliderTrack - - property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0 - property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0 + Loader { + id: loader + anchors.fill: parent + sourceComponent: orientation === DankSlider.Horizontal ? horizontalLayout : verticalLayout + } - width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0)) - height: 12 - radius: Theme.cornerRadius - color: slider.trackColor - anchors.verticalCenter: parent.verticalCenter - clip: false + Component { + id: horizontalLayout - StyledRect { - id: sliderFill - height: parent.height - radius: Theme.cornerRadius - width: { - const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum) - const travel = sliderTrack.width - sliderHandle.width - const center = (travel * ratio) + sliderHandle.width / 2 - return Math.max(0, Math.min(sliderTrack.width, center)) - } - color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) + Row { + anchors.centerIn: parent + width: parent.width + spacing: Theme.spacingM + property bool containsMouse: sliderMouseArea.containsMouse + DankIcon { + name: slider.leftIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 + anchors.verticalCenter: parent.verticalCenter + visible: slider.leftIcon.length > 0 } StyledRect { - id: sliderHandle + id: sliderTrack - property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging + property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0 + property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0 - width: 8 - height: 24 + width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0)) + height: 12 radius: Theme.cornerRadius - x: { - const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum) - const travel = sliderTrack.width - width - return Math.max(0, Math.min(travel, travel * ratio)) - } + color: slider.trackColor anchors.verticalCenter: parent.verticalCenter - color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) - border.width: 3 - border.color: slider.thumbOutlineColor - + clip: false StyledRect { - anchors.fill: parent + id: sliderFill + height: parent.height radius: Theme.cornerRadius - color: Theme.onPrimary - opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0 - visible: opacity > 0 + width: { + const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum) + const travel = sliderTrack.width - sliderHandle.width + const center = (travel * ratio) + sliderHandle.width / 2 + return Math.max(0, Math.min(sliderTrack.width, center)) + } + color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) + } StyledRect { - anchors.centerIn: parent - width: parent.width + 20 - height: parent.height + 20 - radius: width / 2 - color: "transparent" - border.width: 2 - border.color: Theme.primary - opacity: slider.enabled && slider.focus ? 0.3 : 0 - visible: opacity > 0 + id: sliderReference + height: 24 + width: 8 + radius: Theme.cornerRadius + visible: slider.reference && slider.reference < slider.maximum && slider.reference > slider.minimum + anchors.verticalCenter: parent.verticalCenter + color: Theme.withAlpha(Theme.onSurface, 0.12) + border.width: 3 + border.color: slider.thumbOutlineColor + x: { + const ratio = (slider.reference - slider.minimum) / (slider.maximum - slider.minimum) + const travel = sliderTrack.width - width + return Math.max(0, Math.min(travel, travel * ratio)) + } } - Rectangle { - id: ripple - anchors.centerIn: parent - width: 0 - height: 0 - radius: width / 2 - color: Theme.onPrimary - opacity: 0 - - function start() { - opacity = 0.16 - width = 0 - height = 0 - rippleAnimation.start() + StyledRect { + id: sliderHandle + + width: 8 + height: 24 + radius: Theme.cornerRadius + x: { + const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum) + const travel = sliderTrack.width - width + return Math.max(0, Math.min(travel, travel * ratio)) + } + anchors.verticalCenter: parent.verticalCenter + color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) + border.width: 3 + border.color: slider.thumbOutlineColor + + + StyledRect { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.onPrimary + opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0 + visible: opacity > 0 } - SequentialAnimation { - id: rippleAnimation - NumberAnimation { - target: ripple - properties: "width,height" - to: 28 - duration: 180 + StyledRect { + anchors.centerIn: parent + width: parent.width + 20 + height: parent.height + 20 + radius: width / 2 + color: "transparent" + border.width: 2 + border.color: Theme.primary + opacity: slider.enabled && slider.focus ? 0.3 : 0 + visible: opacity > 0 + } + + Rectangle { + id: ripple + anchors.centerIn: parent + width: 0 + height: 0 + radius: width / 2 + color: Theme.onPrimary + opacity: 0 + + function start() { + opacity = 0.16 + width = 0 + height = 0 + rippleAnimation.start() } - NumberAnimation { - target: ripple - property: "opacity" - to: 0 - duration: 150 + + SequentialAnimation { + id: rippleAnimation + NumberAnimation { + target: ripple + properties: "width,height" + to: 28 + duration: 180 + } + NumberAnimation { + target: ripple + property: "opacity" + to: 0 + duration: 150 + } } } - } - TapHandler { - acceptedButtons: Qt.LeftButton - onPressedChanged: { - if (pressed && slider.enabled) { - ripple.start() + TapHandler { + acceptedButtons: Qt.LeftButton + onPressedChanged: { + if (pressed && slider.enabled) { + ripple.start() + } } } - } - scale: active ? 1.05 : 1.0 + scale: active ? 1.05 : 1.0 - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } } } - } - - Item { - id: sliderContainer - - anchors.fill: parent - - MouseArea { - id: sliderMouseArea - property bool isDragging: false + Item { + id: sliderContainer anchors.fill: parent - anchors.topMargin: -10 - anchors.bottomMargin: -10 - hoverEnabled: true - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: slider.enabled - preventStealing: true - acceptedButtons: Qt.LeftButton - onWheel: wheelEvent => { - if (!slider.wheelEnabled) { - wheelEvent.accepted = false - return - } - let step = Math.max(0.5, (maximum - minimum) / 100) - let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + step) : Math.max(minimum, value - step) - newValue = Math.round(newValue) - if (newValue !== value) { - value = newValue - sliderValueChanged(newValue) + + MouseArea { + id: sliderMouseArea + + property bool isDragging: false + + anchors.fill: parent + anchors.topMargin: -10 + anchors.bottomMargin: -10 + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: slider.enabled + preventStealing: true + acceptedButtons: Qt.LeftButton + onWheel: wheelEvent => { + if (!slider.wheelEnabled) { + wheelEvent.accepted = false + return + } + let step = Math.max(0.5, (maximum - minimum) / 100) + let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + step) : Math.max(minimum, value - step) + newValue = Math.round(newValue) + if (newValue !== value) { + value = newValue + sliderValueChanged(newValue) + } + wheelEvent.accepted = true } - wheelEvent.accepted = true - } - onPressed: mouse => { - if (slider.enabled) { - slider.isDragging = true - sliderMouseArea.isDragging = true - updateValueFromPosition(mouse.x) + onPressed: mouse => { + if (slider.enabled) { + slider.isDragging = true + sliderMouseArea.isDragging = true + updateValueFromPosition(mouse.x, sliderHandle, sliderTrack) + } } - } - onReleased: { - if (slider.enabled) { - slider.isDragging = false - sliderMouseArea.isDragging = false - slider.sliderDragFinished(slider.value) + onReleased: { + if (slider.enabled) { + slider.isDragging = false + sliderMouseArea.isDragging = false + slider.sliderDragFinished(slider.value) + } } - } - onPositionChanged: mouse => { - if (pressed && slider.isDragging && slider.enabled) { - updateValueFromPosition(mouse.x) + onPositionChanged: mouse => { + if (pressed && slider.isDragging && slider.enabled) { + updateValueFromPosition(mouse.x, sliderHandle, sliderTrack) + } } + onClicked: mouse => { + if (slider.enabled && !slider.isDragging) { + updateValueFromPosition(mouse.x, sliderHandle, sliderTrack) } - onClicked: mouse => { - if (slider.enabled && !slider.isDragging) { - updateValueFromPosition(mouse.x) } - } + } } + + StyledRect { + id: valueTooltip + + width: tooltipText.contentWidth + Theme.spacingS * 2 + height: tooltipText.contentHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + anchors.bottom: slider.tooltipPlacement === DankSlider.Before ? parent.top : undefined + anchors.top: slider.tooltipPlacement === DankSlider.After ? parent.bottom : undefined + anchors.bottomMargin: Theme.spacingM + anchors.topMargin: Theme.spacingM + x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width/2 - width/2)) + visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue)) + opacity: visible ? 1 : 0 + z: 100 + + StyledText { + id: tooltipText + + text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + font.hintingPreference: Font.PreferFullHinting + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + DankIcon { + name: slider.rightIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 + anchors.verticalCenter: parent.verticalCenter + visible: slider.rightIcon.length > 0 + } + } + } + + Component { + id: verticalLayout + + Column { + anchors.centerIn: parent + height: parent.height + width: parent.width + spacing: Theme.spacingM + property bool containsMouse: sliderMouseArea.containsMouse + + DankIcon { + name: slider.leftIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 + anchors.horizontalCenter: parent.horizontalCenter + visible: slider.leftIcon.length > 0 } StyledRect { - id: valueTooltip + id: sliderTrack - width: tooltipText.contentWidth + Theme.spacingS * 2 - height: tooltipText.contentHeight + Theme.spacingXS * 2 + property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0 + property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0 + + width: 12 + height: parent.height - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0)) radius: Theme.cornerRadius - color: Theme.surfaceContainer - border.color: Theme.outline - border.width: 1 - anchors.bottom: parent.top - anchors.bottomMargin: Theme.spacingM - x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width/2 - width/2)) - visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue)) - opacity: visible ? 1 : 0 - - StyledText { - id: tooltipText - - text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - anchors.centerIn: parent - font.hintingPreference: Font.PreferFullHinting + color: slider.trackColor + anchors.horizontalCenter: parent.horizontalCenter + clip: false + + StyledRect { + id: sliderFill + width: parent.width + radius: Theme.cornerRadius + height: { + const ratio = (slider.value - slider.minimum) / (slider.maximum - slider.minimum) + const travel = sliderTrack.height - sliderHandle.height + const center = (travel * ratio) + sliderHandle.height / 2 + return Math.max(0, Math.min(sliderTrack.height, center)) + } + color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) + anchors.bottom: parent.bottom + } + + StyledRect { + id: sliderReference + width: 24 + height: 8 + radius: Theme.cornerRadius + visible: slider.reference && slider.reference < slider.maximum && slider.reference > slider.minimum + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.withAlpha(Theme.onSurface, 0.12) + border.width: 3 + border.color: slider.thumbOutlineColor + y: { + const ratio = 1 - ((slider.reference - slider.minimum) / (slider.maximum - slider.minimum)) + const travel = sliderTrack.height - height + return Math.max(0, Math.min(travel, travel * ratio)) + } + } + + StyledRect { + id: sliderHandle + + width: 24 + height: 8 + radius: Theme.cornerRadius + y: { + const ratio = 1 - (slider.value - slider.minimum) / (slider.maximum - slider.minimum) + const travel = sliderTrack.height - height + return Math.max(0, Math.min(travel, travel * ratio)) + } + anchors.horizontalCenter: parent.horizontalCenter + color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12) + border.width: 3 + border.color: slider.thumbOutlineColor + + StyledRect { + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.onPrimary + opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0 + visible: opacity > 0 + } + + StyledRect { + anchors.centerIn: parent + width: parent.width + 20 + height: parent.height + 20 + radius: width / 2 + color: "transparent" + border.width: 2 + border.color: Theme.primary + opacity: slider.enabled && slider.focus ? 0.3 : 0 + visible: opacity > 0 + } + + Rectangle { + id: ripple + anchors.centerIn: parent + width: 0 + height: 0 + radius: width / 2 + color: Theme.onPrimary + opacity: 0 + + function start() { + opacity = 0.16 + width = 0 + height = 0 + rippleAnimation.start() + } + + SequentialAnimation { + id: rippleAnimation + NumberAnimation { + target: ripple + properties: "width,height" + to: 28 + duration: 180 + } + NumberAnimation { + target: ripple + property: "opacity" + to: 0 + duration: 150 + } + } + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onPressedChanged: { + if (pressed && slider.enabled) { + ripple.start() + } + } + } + + scale: active ? 1.05 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } } - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing + Item { + id: sliderContainer + + anchors.fill: parent + + MouseArea { + id: sliderMouseArea + + property bool isDragging: false + + anchors.fill: parent + anchors.leftMargin: -10 + anchors.rightMargin: -10 + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: slider.enabled + preventStealing: true + acceptedButtons: Qt.LeftButton + onWheel: wheelEvent => { + if (!slider.wheelEnabled) { + wheelEvent.accepted = false + return + } + let step = Math.max(0.5, (maximum - minimum) / 100) + let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + step) : Math.max(minimum, value - step) + newValue = Math.round(newValue) + if (newValue !== value) { + value = newValue + sliderValueChanged(newValue) + } + wheelEvent.accepted = true + } + onPressed: mouse => { + if (slider.enabled) { + slider.isDragging = true + sliderMouseArea.isDragging = true + updateValueFromPosition(mouse.y, sliderHandle, sliderTrack) + } + } + onReleased: { + if (slider.enabled) { + slider.isDragging = false + sliderMouseArea.isDragging = false + slider.sliderDragFinished(slider.value) + } + } + onPositionChanged: mouse => { + if (pressed && slider.isDragging && slider.enabled) { + updateValueFromPosition(mouse.y, sliderHandle, sliderTrack) + } + } + onClicked: mouse => { + if (slider.enabled && !slider.isDragging) { + updateValueFromPosition(mouse.y, sliderHandle, sliderTrack) + } + } + } + } + + StyledRect { + id: valueTooltip + + width: tooltipText.contentWidth + Theme.spacingS * 2 + height: tooltipText.contentHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + anchors.right: slider.tooltipPlacement === DankSlider.Before ? parent.left : undefined + anchors.left: slider.tooltipPlacement === DankSlider.After ? parent.right : undefined + anchors.rightMargin: Theme.spacingM + anchors.leftMargin: Theme.spacingM + y: Math.max(0, Math.min(parent.height - height, sliderHandle.y + sliderHandle.height/2 - height/2)) + visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue)) + opacity: visible ? 1 : 0 + z: 100 + + StyledText { + id: tooltipText + + text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + font.hintingPreference: Font.PreferFullHinting + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } } } } - } - DankIcon { - name: slider.rightIcon - size: Theme.iconSize - color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 - anchors.verticalCenter: parent.verticalCenter - visible: slider.rightIcon.length > 0 + DankIcon { + name: slider.rightIcon + size: Theme.iconSize + color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38 + anchors.horizontalCenter: parent.horizontalCenter + visible: slider.rightIcon.length > 0 + } } } }