diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 82670296e..2969919c8 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -107,6 +107,12 @@ Singleton { property real y: 280 property int tabIndex: 0 } + property JsonObject fpsLimiter: JsonObject { + property bool pinned: false + property bool clickthrough: false + property real x: 1576 + property real y: 630 + } } property JsonObject timer: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml index df87dcb0e..6e2fd4166 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml @@ -18,5 +18,6 @@ ToolbarButton { iconSize: 22 text: iconBtn.text color: iconBtn.colText + animateChange: true } } diff --git a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml index 5054b9d1c..71ba510c6 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,30 @@ Scope { right: true } + HyprlandFocusGrab { + id: grab + windows: [overlayWindow] + active: false + onCleared: () => { + if (!active) GlobalStates.overlayOpen = false; + } + } + + Connections { + target: GlobalStates + function onOverlayOpenChanged() { + delayedGrabTimer.restart(); + } + } + + Timer { + id: delayedGrabTimer + interval: Appearance.animation.elementMoveFast.duration + onTriggered: { + grab.active = GlobalStates.overlayOpen; + } + } + 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 c13b1933c..b228deda6 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml @@ -9,6 +9,7 @@ Singleton { { identifier: "recorder", materialSymbol: "screen_record" }, { identifier: "volumeMixer", materialSymbol: "volume_up" }, { identifier: "crosshair", materialSymbol: "point_scan" }, + { identifier: "fpsLimiter", materialSymbol: "animation" }, { identifier: "resources", materialSymbol: "browse_activity" } ] diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml index 90f85a7fd..fc75d1455 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 import qs.modules.overlay.recorder import qs.modules.overlay.resources @@ -17,6 +18,7 @@ DelegateChooser { DelegateChoice { roleValue: "crosshair"; Crosshair {} } DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} } + DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} } DelegateChoice { roleValue: "recorder"; Recorder {} } DelegateChoice { roleValue: "resources"; Resources {} } } 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..7628d58be --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml @@ -0,0 +1,12 @@ +import QtQuick +import Quickshell +import qs.modules.common +import qs.modules.overlay + +StyledOverlayWidget { + id: root + title: "MangoHud FPS" + contentItem: FpsLimiterContent { + radius: root.contentRadius + } +} 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..a67bf40c1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml @@ -0,0 +1,97 @@ +import qs.services +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 + + enum State { Normal, Success, Error } + + anchors.fill: parent + property real padding: 16 + property var currentState: FpsLimiterContent.State.Normal + color: Appearance.m3colors.m3surfaceContainer + implicitWidth: content.implicitWidth + (padding * 2) + implicitHeight: content.implicitHeight + (padding * 2) + + Timer { + id: iconResetTimer + interval: 1000 + onTriggered: { + root.currentState = FpsLimiterContent.State.Normal; + } + } + + function applyLimit() { + var fpsValue = parseInt(fpsField.text); + if (isNaN(fpsValue) || fpsValue < 0) { + root.currentState = FpsLimiterContent.State.Error; + iconResetTimer.restart(); + fpsField.text = ""; + 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(); + + root.currentState = FpsLimiterContent.State.Success; + iconResetTimer.restart(); + + // Clear the field after applying + fpsField.text = ""; + } + + Process { + id: fpsSetter + } + + RowLayout { + id: content + anchors.centerIn: parent + spacing: 4 + + ToolbarTextField { + id: fpsField + Layout.fillWidth: true + Layout.preferredWidth: 200 + placeholderText: root.currentState === FpsLimiterContent.State.Error ? Translation.tr("Enter a valid number") : Translation.tr("Set FPS limit") + inputMethodHints: Qt.ImhDigitsOnly + focus: true + + onAccepted: { + root.applyLimit(); + } + } + + IconToolbarButton { + id: applyButton + text: switch (root.currentState) { + case FpsLimiterContent.State.Error: return "close"; + case FpsLimiterContent.State.Success: return "check"; + case FpsLimiterContent.State.Normal: + default: return "save"; + } + enabled: root.currentState === FpsLimiterContent.State.Normal && fpsField.text.length > 0 + onClicked: { + root.applyLimit(); + } + } + } +}