diff --git a/Modules/BrightnessPopup.qml b/Modules/BrightnessPopup.qml new file mode 100644 index 00000000..ac86aebf --- /dev/null +++ b/Modules/BrightnessPopup.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + property var modelData + property bool brightnessPopupVisible: false + property var brightnessDebounceTimer + + brightnessDebounceTimer: Timer { + property int pendingValue: 0 + + interval: BrightnessService.ddcAvailable ? 500 : 50 + repeat: false + onTriggered: { + BrightnessService.setBrightnessInternal(pendingValue); + } + } + + function show() { + root.brightnessPopupVisible = true; + hideTimer.restart(); + } + + function resetHideTimer() { + if (root.brightnessPopupVisible) + hideTimer.restart(); + } + + screen: modelData + visible: brightnessPopupVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Timer { + id: hideTimer + + interval: 3000 + repeat: false + onTriggered: { + if (!brightnessPopup.containsMouse) + root.brightnessPopupVisible = false; + else + hideTimer.restart(); + } + } + + Connections { + function onBrightnessChanged() { + root.show(); + } + + target: BrightnessService + } + + Rectangle { + id: brightnessPopup + + property bool containsMouse: popupMouseArea.containsMouse + + width: Math.min(260, Screen.width - Theme.spacingM * 2) + height: brightnessContent.height + Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingM + color: Theme.popupBackground() + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: root.brightnessPopupVisible ? 1 : 0 + scale: root.brightnessPopupVisible ? 1 : 0.9 + layer.enabled: true + + Column { + id: brightnessContent + + anchors.centerIn: parent + width: parent.width - Theme.spacingS * 2 + spacing: Theme.spacingXS + + Item { + property int gap: Theme.spacingS + + width: parent.width + height: 40 + + Rectangle { + width: Theme.iconSize + height: Theme.iconSize + radius: Theme.iconSize / 2 + color: "transparent" + x: parent.gap + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: BrightnessService.brightnessLevel < 30 ? "brightness_low" : + BrightnessService.brightnessLevel < 70 ? "brightness_medium" : "brightness_high" + size: Theme.iconSize + color: Theme.primary + } + } + + DankSlider { + id: brightnessSlider + + width: parent.width - Theme.iconSize - parent.gap * 3 + height: 40 + x: parent.gap * 2 + Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + minimum: 1 + maximum: 100 + enabled: BrightnessService.brightnessAvailable + showValue: true + unit: "%" + Component.onCompleted: { + if (BrightnessService.brightnessAvailable) + value = BrightnessService.brightnessLevel; + } + onSliderValueChanged: function(newValue) { + if (BrightnessService.brightnessAvailable) { + brightnessDebounceTimer.pendingValue = newValue; + brightnessDebounceTimer.restart(); + root.resetHideTimer(); + } + } + onSliderDragFinished: function(finalValue) { + if (BrightnessService.brightnessAvailable) { + brightnessDebounceTimer.stop(); + BrightnessService.setBrightnessInternal(finalValue); + } + } + + Connections { + function onBrightnessChanged() { + brightnessSlider.value = BrightnessService.brightnessLevel; + } + + target: BrightnessService + } + } + } + } + + MouseArea { + id: popupMouseArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + + transform: Translate { + y: root.brightnessPopupVisible ? 0 : 20 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on transform { + PropertyAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + mask: Region { + item: brightnessPopup + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Audio/VolumeControl.qml b/Modules/ControlCenter/Audio/VolumeControl.qml index 4ac916e3..e7977000 100644 --- a/Modules/ControlCenter/Audio/VolumeControl.qml +++ b/Modules/ControlCenter/Audio/VolumeControl.qml @@ -46,7 +46,7 @@ Column { let leftIconItem = volumeSlider.children[0].children[0]; if (leftIconItem) { let mouseArea = Qt.createQmlObject( - 'import QtQuick 2.15; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }', + 'import QtQuick; import qs.Services; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }', leftIconItem, "dynamicMouseArea" ); diff --git a/Modules/ControlCenter/ControlCenterPopout.qml b/Modules/ControlCenter/ControlCenterPopout.qml index 17dd8b1a..18f0af51 100644 --- a/Modules/ControlCenter/ControlCenterPopout.qml +++ b/Modules/ControlCenter/ControlCenterPopout.qml @@ -711,7 +711,7 @@ PanelWindow { interval: BrightnessService.ddcAvailable ? 500 : 50 repeat: false onTriggered: { - BrightnessService.setBrightness(pendingValue); + BrightnessService.setBrightnessInternal(pendingValue); } } @@ -745,7 +745,7 @@ PanelWindow { } onSliderDragFinished: function(finalValue) { parent.parent.brightnessDebounceTimer.stop(); - BrightnessService.setBrightness(finalValue); + BrightnessService.setBrightnessInternal(finalValue); } } diff --git a/Modules/ControlCenter/DisplayTab.qml b/Modules/ControlCenter/DisplayTab.qml index 3ccc0d04..620d76f8 100644 --- a/Modules/ControlCenter/DisplayTab.qml +++ b/Modules/ControlCenter/DisplayTab.qml @@ -20,7 +20,7 @@ ScrollView { repeat: false onTriggered: { - BrightnessService.setBrightness(pendingValue); + BrightnessService.setBrightnessInternal(pendingValue); } } @@ -55,7 +55,7 @@ ScrollView { onSliderDragFinished: function(finalValue) { brightnessDebounceTimer.stop(); - BrightnessService.setBrightness(finalValue); + BrightnessService.setBrightnessInternal(finalValue); } } diff --git a/Modules/MicMutePopup.qml b/Modules/MicMutePopup.qml new file mode 100644 index 00000000..dc22a725 --- /dev/null +++ b/Modules/MicMutePopup.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + property var modelData + property bool micPopupVisible: false + + function show() { + root.micPopupVisible = true; + hideTimer.restart(); + } + + screen: modelData + visible: micPopupVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Timer { + id: hideTimer + + interval: 2000 + repeat: false + onTriggered: { + root.micPopupVisible = false; + } + } + + Connections { + function onMicMuteChanged() { + root.show(); + } + + target: AudioService + } + + Rectangle { + id: micPopup + + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingM + color: Theme.popupBackground() + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + opacity: root.micPopupVisible ? 1 : 0 + scale: root.micPopupVisible ? 1 : 0.9 + layer.enabled: true + + DankIcon { + id: micContent + + anchors.centerIn: parent + name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic" + size: Theme.iconSize + color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary + } + + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + + transform: Translate { + y: root.micPopupVisible ? 0 : 20 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on transform { + PropertyAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + mask: Region { + item: micPopup + } +} \ No newline at end of file diff --git a/README.md b/README.md index 84d06d87..4b82d458 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,10 @@ Add to your niri config spawn-at-startup "qs" "-c" "DankMaterialShell" // Dank keybinds +// 1. These should not be in conflict with any pre-existing keybindings +// 2. You need to merge them with your existing config if you want to use these +// 3. You can change the keys to whatever you want, if you prefer something different +// 4. For the increment/decrement ones you can change the steps to whatever you like too binds { Mod+Space hotkey-overlay-title="Application Launcher" { spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "spotlight" "toggle"; @@ -197,6 +201,12 @@ binds { XF86AudioMicMute allow-when-locked=true { spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "audio" "micmute"; } + XF86MonBrightnessUp allow-when-locked=true { + spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "increment" "5"; + } + XF86MonBrightnessDown allow-when-locked=true { + spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5"; + } } ``` diff --git a/Services/AudioService.qml b/Services/AudioService.qml index db88e739..5eaa7b91 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -13,6 +13,7 @@ Singleton { readonly property PwNode source: Pipewire.defaultAudioSource signal volumeChanged() + signal micMuteChanged() function displayName(node) { if (!node) return "" @@ -137,7 +138,9 @@ Singleton { } function mute(): string { - return root.toggleMute(); + const result = root.toggleMute(); + root.volumeChanged(); + return result; } function setmic(percentage: string): string { @@ -145,7 +148,9 @@ Singleton { } function micmute(): string { - return root.toggleMicMute(); + const result = root.toggleMicMute(); + root.micMuteChanged(); + return result; } function status(): string { diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index a6465813..8daa348e 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -16,7 +16,9 @@ Singleton { property int currentRawBrightness: 0 property bool brightnessInitialized: false - function setBrightness(percentage) { + signal brightnessChanged() + + function setBrightnessInternal(percentage) { brightnessLevel = Math.max(1, Math.min(100, percentage)); if (laptopBacklightAvailable) { @@ -28,6 +30,11 @@ Singleton { } } + function setBrightness(percentage) { + setBrightnessInternal(percentage); + brightnessChanged(); + } + Component.onCompleted: { ddcAvailabilityChecker.running = true; laptopBacklightChecker.running = true; @@ -143,4 +150,51 @@ Singleton { } } } + + // IPC Handler for external control + IpcHandler { + target: "brightness" + + function set(percentage: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available"; + } + + const value = parseInt(percentage); + const clampedValue = Math.max(1, Math.min(100, value)); + root.setBrightness(clampedValue); + return "Brightness set to " + clampedValue + "%"; + } + + function increment(step: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available"; + } + + const currentLevel = root.brightnessLevel; + const newLevel = Math.max(1, Math.min(100, currentLevel + parseInt(step || "10"))); + root.setBrightness(newLevel); + return "Brightness increased to " + newLevel + "%"; + } + + function decrement(step: string): string { + if (!root.brightnessAvailable) { + return "Brightness control not available"; + } + + const currentLevel = root.brightnessLevel; + const newLevel = Math.max(1, Math.min(100, currentLevel - parseInt(step || "10"))); + root.setBrightness(newLevel); + return "Brightness decreased to " + newLevel + "%"; + } + + function status(): string { + if (!root.brightnessAvailable) { + return "Brightness control not available"; + } + + return "Brightness: " + root.brightnessLevel + "% (" + + (root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")"; + } + } } \ No newline at end of file diff --git a/shell.qml b/shell.qml index 01fba7e7..3818123a 100644 --- a/shell.qml +++ b/shell.qml @@ -189,4 +189,22 @@ ShellRoot { } + Variants { + model: Quickshell.screens + + delegate: MicMutePopup { + modelData: item + } + + } + + Variants { + model: Quickshell.screens + + delegate: BrightnessPopup { + modelData: item + } + + } + }