From 60144ca3debb8a2ad979ccd384b4fe1867a5e42b Mon Sep 17 00:00:00 2001 From: reakjra Date: Thu, 6 Nov 2025 14:23:13 +0100 Subject: [PATCH] Feature (Overlay): MangoHud Fps Limiter widget --- .../ii/modules/common/Persistent.qml | 6 ++ .../quickshell/ii/modules/overlay/Overlay.qml | 12 ++- .../ii/modules/overlay/OverlayContent.qml | 1 + .../ii/modules/overlay/OverlayContext.qml | 1 + .../overlay/OverlayWidgetDelegateChooser.qml | 2 + .../modules/overlay/fpsLimiter/FpsLimiter.qml | 10 ++ .../overlay/fpsLimiter/FpsLimiterContent.qml | 92 +++++++++++++++++++ 7 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml create mode 100644 dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 814938452..7e077b2c3 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -93,6 +93,12 @@ Singleton { property real x: 55 property real y: 188 } + property JsonObject fpsLimiter: JsonObject { + property bool pinned: false + property bool clickthrough: false + property real x: 100 + property real y: 100 + } } property JsonObject timer: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml index 5054b9d1c..2fd33fcd7 100644 --- a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml +++ b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml @@ -25,7 +25,7 @@ Scope { exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:overlay" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None visible: true color: "transparent" @@ -43,6 +43,16 @@ Scope { right: true } + HyprlandFocusGrab { + id: grab + windows: [overlayWindow] + active: GlobalStates.overlayOpen + onCleared: () => { + if (!active) + GlobalStates.overlayOpen = false; + } + } + OverlayContent { id: overlayContent anchors.fill: parent diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml index 838267b80..546962185 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml @@ -12,6 +12,7 @@ import qs.modules.overlay.crosshair Item { id: root + focus: true readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true Keys.onPressed: (event) => { // Esc to close diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml index 1417c8467..cc54d379f 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml @@ -7,6 +7,7 @@ Singleton { readonly property list availableWidgets: [ { identifier: "crosshair", materialSymbol: "point_scan" }, + { identifier: "fpsLimiter", materialSymbol: "speed" }, { identifier: "volumeMixer", materialSymbol: "volume_up" } ] diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml index e7420f625..dffdfaaee 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml @@ -8,6 +8,7 @@ import Quickshell import Quickshell.Bluetooth import qs.modules.overlay.crosshair import qs.modules.overlay.volumeMixer +import qs.modules.overlay.fpsLimiter DelegateChooser { id: root @@ -15,4 +16,5 @@ DelegateChooser { DelegateChoice { roleValue: "crosshair"; Crosshair {} } DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} } + DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} } } diff --git a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml new file mode 100644 index 000000000..fe3ab9df4 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml @@ -0,0 +1,10 @@ +import QtQuick +import Quickshell +import qs.modules.common +import qs.modules.overlay + +StyledOverlayWidget { + id: root + title: "FPS Limiter" + contentItem: FpsLimiterContent {} +} diff --git a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml new file mode 100644 index 000000000..fae1aca78 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.widgets + +Rectangle { + id: root + color: Appearance.m3colors.m3surfaceContainer + property real padding: 20 + implicitWidth: contentColumn.implicitWidth + padding * 2 + implicitHeight: contentColumn.implicitHeight + padding * 2 + + function applyLimit() { + var fpsValue = parseInt(fpsField.text); + if (isNaN(fpsValue) || fpsValue < 0) { + return; + } + + var cfgPaths = [ + "~/.config/MangoHud/MangoHud.conf", + ]; // MangoHud config files + + var updateCommands = cfgPaths.map(path => { + return "if grep -q '^fps_limit=' " + path + "; " + + "then sed -i 's/^fps_limit=.*/fps_limit=" + fpsValue + "/' " + path + "; " + + "else echo 'fps_limit=" + fpsValue + "' >> " + path + "; fi"; + }).join("; "); + + var cmd = updateCommands + "; pkill -SIGUSR2 mangohud"; + + fpsSetter.command = ["bash", "-c", cmd]; + fpsSetter.startDetached(); + + // Clear the field after applying + fpsField.text = ""; + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + fpsField.text = ""; + event.accepted = true; + } + } + + ColumnLayout { + id: contentColumn + anchors.centerIn: parent + spacing: 15 + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + ToolbarTextField { + id: fpsField + Layout.fillWidth: true + Layout.preferredWidth: 200 + placeholderText: qsTr("Set FPS limit (e.g. 80)") + inputMethodHints: Qt.ImhDigitsOnly + focus: true + + Keys.onReturnPressed: { + root.applyLimit(); + event.accepted = true; + } + } + + RippleButton { + id: applyButton + implicitWidth: 36 + implicitHeight: 36 + buttonRadius: Appearance.rounding.full + onClicked: { + root.applyLimit(); + } + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.title + text: "keyboard_return" + } + } + } + + Process { + id: fpsSetter + } + } +}