mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 09:14:01 -04:00
hyprland: add focus follows mouse toggle (#3755)
This commit is contained in:
@@ -245,6 +245,7 @@ Singleton {
|
||||
property int mangoLayoutGapsOutOverride: Spec.SPEC.mangoLayoutGapsOutOverride.def
|
||||
property int mangoLayoutRadiusOverride: Spec.SPEC.mangoLayoutRadiusOverride.def
|
||||
property int mangoLayoutBorderSize: Spec.SPEC.mangoLayoutBorderSize.def
|
||||
property int hyprlandFollowMouse: Spec.SPEC.hyprlandFollowMouse.def
|
||||
property string mouseAccelProfile: Spec.SPEC.mouseAccelProfile.def
|
||||
property real mouseAccelSpeed: Spec.SPEC.mouseAccelSpeed.def
|
||||
property bool mouseLeftHanded: Spec.SPEC.mouseLeftHanded.def
|
||||
|
||||
@@ -315,6 +315,10 @@ var LOCAL_SPEC = {
|
||||
def: -1,
|
||||
onChange: "updateCompositorLayout"
|
||||
},
|
||||
hyprlandFollowMouse: {
|
||||
def: -1,
|
||||
onChange: "updateCompositorInput"
|
||||
},
|
||||
mouseAccelProfile: {
|
||||
def: "default",
|
||||
onChange: "updateCompositorInput"
|
||||
|
||||
@@ -32,6 +32,18 @@ Item {
|
||||
settingKey: "mouseSettings"
|
||||
iconName: "mouse"
|
||||
|
||||
SettingsToggleRow {
|
||||
visible: CompositorService.isHyprland && HyprlandService.hyprSupports("input:follow_mouse")
|
||||
enabled: inputInclude.included && !inputInclude.readOnly && HyprlandService.luaConfigActive && HyprlandService.followMouseMode >= 0
|
||||
tags: ["mouse", "focus", "follow", "pointer", "window", "hyprland"]
|
||||
settingKey: "hyprlandFollowMouse"
|
||||
resetKeys: ["hyprlandFollowMouse"]
|
||||
text: I18n.tr("Focus follows mouse", "Hyprland toggle that focuses a window when the pointer moves over it")
|
||||
description: I18n.tr("Focus windows when the pointer moves over them.", "Description of the Hyprland focus follows mouse toggle")
|
||||
checked: (SettingsData.hyprlandFollowMouse < 0 ? HyprlandService.followMouseMode : SettingsData.hyprlandFollowMouse) === 1
|
||||
onToggled: checked => SettingsData.set("hyprlandFollowMouse", checked ? 1 : 0)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tags: ["mouse", "sensitivity", "speed", "accel"]
|
||||
settingKey: "mouseAccelSpeed"
|
||||
|
||||
@@ -42,6 +42,7 @@ Singleton {
|
||||
property bool hyprOptionsLoading: false
|
||||
property bool inputGenerationPending: false
|
||||
property bool inputGenerationRunning: false
|
||||
property int followMouseMode: -1
|
||||
// Hyprland only has per-device touchpad speed; probed once per compositor, a hotplugged touchpad waits for the next shell start
|
||||
property var hyprTouchpadNames: []
|
||||
property bool hyprDevicesReady: false
|
||||
@@ -106,6 +107,7 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isHyprland) {
|
||||
refreshFollowMouse();
|
||||
refreshWorkspaceRules();
|
||||
refreshLuaConfigStatus();
|
||||
if (luaConfigActive)
|
||||
@@ -144,12 +146,14 @@ Singleton {
|
||||
target: CompositorService
|
||||
function onIsHyprlandChanged() {
|
||||
if (CompositorService.isHyprland) {
|
||||
refreshFollowMouse();
|
||||
refreshLuaConfigStatus();
|
||||
if (luaConfigActive)
|
||||
ensureDmsLuaConfigs();
|
||||
generateLayoutConfig();
|
||||
return;
|
||||
}
|
||||
followMouseMode = -1;
|
||||
luaConfigDetected = false;
|
||||
luaConfigStatusReady = false;
|
||||
luaConfigStatusLoading = false;
|
||||
@@ -219,6 +223,30 @@ Singleton {
|
||||
return hyprOptionNames[name] === true;
|
||||
}
|
||||
|
||||
function refreshFollowMouse() {
|
||||
if (!CompositorService.isHyprland)
|
||||
return;
|
||||
Proc.runCommand("hypr-follow-mouse", ["hyprctl", "-j", "getoption", "input:follow_mouse"], (output, exitCode) => {
|
||||
if (exitCode !== 0 || !CompositorService.isHyprland)
|
||||
return;
|
||||
try {
|
||||
const mode = JSON.parse(output).int;
|
||||
if (Number.isInteger(mode) && mode >= 0 && mode <= 3)
|
||||
followMouseMode = mode;
|
||||
} catch (error) {
|
||||
log.warn("hyprctl follow_mouse parse failed:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService.isHyprland ? Hyprland : null
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "configreloaded")
|
||||
root.refreshFollowMouse();
|
||||
}
|
||||
}
|
||||
|
||||
function loadHyprOptionNames() {
|
||||
if (!CompositorService.isHyprland || hyprOptionsReady || hyprOptionsLoading)
|
||||
return;
|
||||
@@ -819,6 +847,9 @@ hl.layer_rule({
|
||||
if (hyprSupports(option))
|
||||
lines.push(line);
|
||||
};
|
||||
// -1 leaves existing follow_mouse modes untouched until the user operates the toggle.
|
||||
if (s.hyprlandFollowMouse === 0 || s.hyprlandFollowMouse === 1)
|
||||
add("input:follow_mouse", `follow_mouse = ${s.hyprlandFollowMouse},`);
|
||||
if (s.mouseAccelSpeed !== 0)
|
||||
add("input:sensitivity", `sensitivity = ${s.mouseAccelSpeed.toFixed(2)},`);
|
||||
if (s.mouseAccelProfile !== "default")
|
||||
|
||||
@@ -24,7 +24,7 @@ const scope = vm.createContext({
|
||||
liveMonitor: name => live[name] ?? null,
|
||||
hyprOptionNames: new Proxy({}, { get: (names, key) => key in names ? names[key] : true })
|
||||
});
|
||||
for (const fn of ["getOutputIdentifier", "luaQuoted", "forceFlagValue", "buildOutputsLua", "luaString", "luaTable", "hyprSupports", "blurLines", "decorationLines", "groupbarLines", "hyprScrollMethod", "touchpadNames", "inputLines", "touchpadDeviceLines"])
|
||||
for (const fn of ["getOutputIdentifier", "luaQuoted", "forceFlagValue", "buildOutputsLua", "luaString", "luaTable", "hyprSupports", "blurLines", "decorationLines", "groupbarLines", "hyprScrollMethod", "touchpadNames", "inputLines", "touchpadDeviceLines", "refreshFollowMouse"])
|
||||
vm.runInContext(extract(fn), scope);
|
||||
const withOptions = (names, run) => {
|
||||
const previous = scope.hyprOptionNames;
|
||||
@@ -144,6 +144,36 @@ test("input: turning natural scroll off is written so the template's true cannot
|
||||
assert.ok(table(inputLines(input({ touchpadNaturalScroll: false })), "touchpad").includes("natural_scroll = false,"));
|
||||
});
|
||||
|
||||
test("input: focus follows mouse writes both on and off, leaves unmanaged modes alone, and skips unsupported options", () => {
|
||||
const modeLine = mode => inputLines(input({ hyprlandFollowMouse: mode })).filter(line => line.startsWith("follow_mouse ="));
|
||||
assert.deepEqual(modeLine(1), ["follow_mouse = 1,"]);
|
||||
assert.deepEqual(modeLine(0), ["follow_mouse = 0,"]);
|
||||
assert.deepEqual(modeLine(-1), []);
|
||||
withOptions({}, () => {
|
||||
assert.deepEqual(modeLine(1), []);
|
||||
});
|
||||
});
|
||||
|
||||
test("input: the live focus mode accepts all Hyprland modes and keeps the last value on failed reads", () => {
|
||||
scope.CompositorService = { isHyprland: true };
|
||||
scope.log = { warn() {} };
|
||||
let callback;
|
||||
scope.Proc = { runCommand(id, args, cb) { callback = cb; } };
|
||||
for (const mode of [0, 1, 2, 3]) {
|
||||
scope.refreshFollowMouse();
|
||||
callback(JSON.stringify({ int: mode }), 0);
|
||||
assert.equal(scope.followMouseMode, mode);
|
||||
}
|
||||
for (const [output, status] of [["", 1], ["invalid", 0], ['{"int":4}', 0], ['{"int":"1"}', 0]]) {
|
||||
scope.refreshFollowMouse();
|
||||
callback(output, status);
|
||||
assert.equal(scope.followMouseMode, 3);
|
||||
}
|
||||
scope.CompositorService.isHyprland = false;
|
||||
callback('{"int":1}', 0);
|
||||
assert.equal(scope.followMouseMode, 3);
|
||||
});
|
||||
|
||||
test("input: keyboard keys stay out until one is configured, then numlock is explicit and kb_file replaces the xkb names", () => {
|
||||
const untouched = inputLines(input({ keyboardTrackLayout: "window" }));
|
||||
assert.equal(untouched.some(line => /^(kb_|repeat_|numlock)/.test(line)), false);
|
||||
|
||||
@@ -49,3 +49,15 @@ test("a coerced key normalises a percentage and leaves a unit value alone", () =
|
||||
assert.equal(loaded({ popupTransparency: 50 }).popupTransparency, 0.5);
|
||||
assert.equal(loaded({ popupTransparency: 0.5 }).popupTransparency, 0.5);
|
||||
});
|
||||
|
||||
test("a focus override persists both off and on, and resetting removes it", () => {
|
||||
const root = defaultRoot();
|
||||
for (const mode of [0, 1]) {
|
||||
root.hyprlandFollowMouse = mode;
|
||||
const json = store.toJson(root, new Set());
|
||||
assert.equal(json.hyprlandFollowMouse, mode);
|
||||
assert.equal(loaded(json).hyprlandFollowMouse, mode);
|
||||
}
|
||||
root.hyprlandFollowMouse = -1;
|
||||
assert.equal("hyprlandFollowMouse" in store.toJson(root, new Set()), false);
|
||||
});
|
||||
|
||||
@@ -14375,6 +14375,29 @@
|
||||
"parentLabel": "Input",
|
||||
"conditionKey": "pointerCapable"
|
||||
},
|
||||
{
|
||||
"section": "hyprlandFollowMouse",
|
||||
"label": "Focus follows mouse",
|
||||
"tabIndex": 44,
|
||||
"category": "Mouse & touchpad",
|
||||
"keywords": [
|
||||
"focus",
|
||||
"follow",
|
||||
"follows",
|
||||
"hyprland",
|
||||
"input",
|
||||
"mouse",
|
||||
"moves",
|
||||
"over",
|
||||
"pointer",
|
||||
"touchpad",
|
||||
"window",
|
||||
"windows"
|
||||
],
|
||||
"parentLabel": "Input",
|
||||
"description": "Focus windows when the pointer moves over them.",
|
||||
"conditionKey": "isHyprland"
|
||||
},
|
||||
{
|
||||
"section": "mouseLeftHanded",
|
||||
"label": "Left-handed",
|
||||
@@ -14431,14 +14454,19 @@
|
||||
"category": "Mouse & touchpad",
|
||||
"keywords": [
|
||||
"acceleration",
|
||||
"focus",
|
||||
"input",
|
||||
"mouse",
|
||||
"moves",
|
||||
"over",
|
||||
"pointer",
|
||||
"sensitivity",
|
||||
"touchpad"
|
||||
"touchpad",
|
||||
"windows"
|
||||
],
|
||||
"parentLabel": "Input",
|
||||
"icon": "mouse",
|
||||
"description": "Focus windows when the pointer moves over them.",
|
||||
"conditionKey": "inputCapable"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user