mirror of
https://github.com/hyprwm/Hyprland.git
synced 2026-08-24 10:04:19 -05:00
keybinds: fix subchord matching for release binds (#15904)
This commit is contained in:
@@ -832,6 +832,86 @@ TEST_CASE(keybinds) {
|
||||
CALL_SUBTEST(unbind);
|
||||
}
|
||||
|
||||
TEST_CASE(unorderedSubChordDeferral) {
|
||||
constexpr uint32_t X = KEY_X + 8;
|
||||
constexpr uint32_t D = KEY_D + 8;
|
||||
constexpr uint32_t F = KEY_F + 8;
|
||||
|
||||
const auto counts = [] { return getFromSocket("/repl return _G.hyprtester_overlap_x .. ':' .. _G.hyprtester_overlap_d .. ':' .. _G.hyprtester_overlap_long"); };
|
||||
|
||||
OK(getFromSocket("/eval do "
|
||||
"_G.hyprtester_overlap_x = 0; "
|
||||
"_G.hyprtester_overlap_d = 0; "
|
||||
"_G.hyprtester_overlap_long = 0; "
|
||||
"hl.bind('SUPER + X', function() _G.hyprtester_overlap_x = _G.hyprtester_overlap_x + 1 end); "
|
||||
"hl.bind('SUPER + D', function() _G.hyprtester_overlap_d = _G.hyprtester_overlap_d + 1 end); "
|
||||
"hl.bind('SUPER + X + D + F', function() _G.hyprtester_overlap_long = _G.hyprtester_overlap_long + 1 end) "
|
||||
"end"));
|
||||
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_META, X)));
|
||||
EXPECT(counts(), "0:0:0");
|
||||
OK(getFromSocket(pluginKeybindCmd(false, 0, X)));
|
||||
EXPECT(counts(), "1:0:0");
|
||||
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_META, D)));
|
||||
EXPECT(counts(), "1:0:0");
|
||||
OK(getFromSocket(pluginKeybindCmd(false, 0, D)));
|
||||
EXPECT(counts(), "1:1:0");
|
||||
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_META, X)));
|
||||
EXPECT(counts(), "1:1:0");
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_META, D)));
|
||||
EXPECT(counts(), "1:1:0");
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_META, F)));
|
||||
EXPECT(counts(), "1:1:1");
|
||||
OK(getFromSocket(pluginKeybindCmd(false, MOD_META, F)));
|
||||
OK(getFromSocket(pluginKeybindCmd(false, MOD_META, D)));
|
||||
OK(getFromSocket(pluginKeybindCmd(false, 0, X)));
|
||||
EXPECT(counts(), "1:1:1");
|
||||
|
||||
OK(getFromSocket("/eval do "
|
||||
"hl.unbind('SUPER + X'); "
|
||||
"hl.unbind('SUPER + D'); "
|
||||
"hl.unbind('SUPER + X + D + F'); "
|
||||
"_G.hyprtester_overlap_x = nil; "
|
||||
"_G.hyprtester_overlap_d = nil; "
|
||||
"_G.hyprtester_overlap_long = nil "
|
||||
"end"));
|
||||
}
|
||||
|
||||
TEST_CASE(modifierReleaseBindShadowing) {
|
||||
constexpr uint32_t ALT_L = KEY_LEFTALT + 8;
|
||||
constexpr uint32_t P = KEY_P + 8;
|
||||
|
||||
const auto counts = [] { return getFromSocket("/repl return _G.hyprtester_alt_release .. ':' .. _G.hyprtester_alt_p"); };
|
||||
|
||||
OK(getFromSocket("/eval do "
|
||||
"_G.hyprtester_alt_release = 0; "
|
||||
"_G.hyprtester_alt_p = 0; "
|
||||
"hl.bind('ALT + ALT_L', function() _G.hyprtester_alt_release = _G.hyprtester_alt_release + 1 end, { release = true }); "
|
||||
"hl.bind('ALT + P', function() _G.hyprtester_alt_p = _G.hyprtester_alt_p + 1 end) "
|
||||
"end"));
|
||||
|
||||
OK(getFromSocket(pluginKeybindCmd(true, 0, ALT_L)));
|
||||
EXPECT(counts(), "0:0");
|
||||
OK(getFromSocket(pluginKeybindCmd(true, MOD_ALT, P)));
|
||||
EXPECT(counts(), "0:1");
|
||||
OK(getFromSocket(pluginKeybindCmd(false, MOD_ALT, P)));
|
||||
OK(getFromSocket(pluginKeybindCmd(false, 0, ALT_L)));
|
||||
EXPECT(counts(), "0:1");
|
||||
|
||||
OK(getFromSocket(pluginKeybindCmd(true, 0, ALT_L)));
|
||||
OK(getFromSocket(pluginKeybindCmd(false, 0, ALT_L)));
|
||||
EXPECT(counts(), "1:1");
|
||||
|
||||
OK(getFromSocket("/eval do "
|
||||
"hl.unbind('ALT + ALT_L'); "
|
||||
"hl.unbind('ALT + P'); "
|
||||
"_G.hyprtester_alt_release = nil; "
|
||||
"_G.hyprtester_alt_p = nil "
|
||||
"end"));
|
||||
}
|
||||
|
||||
TEST_CASE(luaDispatcherStrings) {
|
||||
OK(getFromSocket("/eval B = hl.bind('SUPER + F24', hl.dsp.exec_cmd('true'))"));
|
||||
EXPECT(getFromSocket("/repl return B.handler"), "HL.Dispatcher(exec_cmd)");
|
||||
|
||||
@@ -315,35 +315,6 @@ bool CBind::isSubChordOf(const CBind& other, const SBindEventContext& ctx) const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CBind::isOrderedPrefixOf(const CBind& other, const SBindEventContext& ctx) const {
|
||||
const auto CHORD_SIZE = chordSize();
|
||||
if (CHORD_SIZE == 0 || CHORD_SIZE >= other.chordSize() || !ctx.trigger || !m_keys.back().matches(*ctx.trigger))
|
||||
return false;
|
||||
|
||||
size_t keyIdx = 0;
|
||||
size_t otherKeyIdx = 0;
|
||||
size_t relevant = 0;
|
||||
for (const auto& held : ctx.heldKeys) {
|
||||
if (held.modifier || std::ranges::none_of(other.m_keys, [&held](const auto& pattern) { return !pattern.isMod() && pattern.matches(held); }))
|
||||
continue;
|
||||
|
||||
if (relevant == CHORD_SIZE)
|
||||
return false;
|
||||
while (m_keys[keyIdx].isMod())
|
||||
++keyIdx;
|
||||
while (other.m_keys[otherKeyIdx].isMod())
|
||||
++otherKeyIdx;
|
||||
if (!m_keys[keyIdx].matches(held) || !other.m_keys[otherKeyIdx].matches(held))
|
||||
return false;
|
||||
|
||||
++keyIdx;
|
||||
++otherKeyIdx;
|
||||
++relevant;
|
||||
}
|
||||
|
||||
return relevant == CHORD_SIZE;
|
||||
}
|
||||
|
||||
std::span<const CKey> CBind::keys() const {
|
||||
return m_keys;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ namespace Keybinds {
|
||||
bool containsKey(const SResolvedKey& key) const;
|
||||
bool isFullyHeld(const SBindEventContext& ctx) const;
|
||||
bool isSubChordOf(const CBind& other, const SBindEventContext& ctx) const;
|
||||
bool isOrderedPrefixOf(const CBind& other, const SBindEventContext& ctx) const;
|
||||
std::span<const CKey> keys() const;
|
||||
std::span<const std::string> keyNames() const;
|
||||
const std::unordered_set<std::string>& devices() const;
|
||||
|
||||
@@ -765,6 +765,10 @@ void CKeybindManager::shadowBinds(const std::optional<SResolvedKey>& excluded, c
|
||||
[&bind](const auto& input) { return std::ranges::any_of(input.deferredBinds, [&bind](const auto& weak) { return weak.lock() == bind; }); }))
|
||||
continue;
|
||||
|
||||
// Multi-key release binds retain their armed state as earlier chord keys are released.
|
||||
// Their overlap suppression is tracked on the armed input instead of through m_shadowed.
|
||||
const bool MATCH_RELEASE = bind->hasFlag(BIND_FLAG_RELEASE) && bind->chordSize() <= 1;
|
||||
|
||||
for (const auto& input : m_inputState.pressed()) {
|
||||
const auto device = input.device.lock();
|
||||
if (!device)
|
||||
@@ -773,15 +777,17 @@ void CKeybindManager::shadowBinds(const std::optional<SResolvedKey>& excluded, c
|
||||
if (excluded && device == excludedDevice && input.key.code == excluded->code && input.key.event == excluded->event)
|
||||
continue;
|
||||
|
||||
if (bind->matches({
|
||||
const SBindEventContext CONTEXT{
|
||||
.heldKeys = m_inputState.heldKeys(),
|
||||
.trigger = input.key,
|
||||
.modifiersNow = MODIFIERS,
|
||||
.modifiersAtPress = input.modifiersAtPress,
|
||||
.pressed = true,
|
||||
.pressed = !MATCH_RELEASE,
|
||||
.device = device,
|
||||
.submap = input.submapAtPress,
|
||||
}) == BIND_MATCH_FULL) {
|
||||
};
|
||||
|
||||
if (bind->matches(CONTEXT) == BIND_MATCH_FULL) {
|
||||
m_shadowed.emplace(bind);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ SChordMatchResolution Keybinds::resolveChordMatches(std::span<const SBindMatchCa
|
||||
continue;
|
||||
|
||||
const bool EXTENDABLE = context.pressed &&
|
||||
std::ranges::any_of(candidates, [&](const auto& other) { return other.match == BIND_MATCH_PARTIAL && candidate.bind->isOrderedPrefixOf(*other.bind, context); });
|
||||
std::ranges::any_of(candidates, [&](const auto& other) { return other.match == BIND_MATCH_PARTIAL && candidate.bind->isSubChordOf(*other.bind, context); });
|
||||
if (EXTENDABLE)
|
||||
result.deferred.emplace_back(candidate.bind);
|
||||
else
|
||||
|
||||
@@ -63,7 +63,7 @@ TEST(KeybindsMatchResolver, FullPrefixWaitsForLongerChord) {
|
||||
EXPECT_EQ(RESOLUTION.deferred.front(), SHORT);
|
||||
}
|
||||
|
||||
TEST(KeybindsMatchResolver, ReverseOrderDoesNotDeferShortChord) {
|
||||
TEST(KeybindsMatchResolver, NonPrefixSubChordWaitsForLongerChord) {
|
||||
const auto SHORT = makeResolverBind({"SUPER", "Q"});
|
||||
const auto LONG = makeResolverBind({"SUPER", "K", "Q"});
|
||||
const auto Q = resolverKey("Q", 24);
|
||||
@@ -79,9 +79,37 @@ TEST(KeybindsMatchResolver, ReverseOrderDoesNotDeferShortChord) {
|
||||
};
|
||||
|
||||
const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT);
|
||||
ASSERT_EQ(RESOLUTION.immediate.size(), 1);
|
||||
EXPECT_EQ(RESOLUTION.immediate.front(), SHORT);
|
||||
EXPECT_TRUE(RESOLUTION.deferred.empty());
|
||||
EXPECT_TRUE(RESOLUTION.immediate.empty());
|
||||
ASSERT_EQ(RESOLUTION.deferred.size(), 1);
|
||||
EXPECT_EQ(RESOLUTION.deferred.front(), SHORT);
|
||||
}
|
||||
|
||||
TEST(KeybindsMatchResolver, MiddleSubChordWaitsForLongerChord) {
|
||||
const auto SHORT = makeResolverBind({"SUPER", "D"});
|
||||
const auto LONG = makeResolverBind({"SUPER", "X", "D", "F"});
|
||||
const auto X = resolverKey("X", 53);
|
||||
const auto D = resolverKey("D", 40);
|
||||
const std::array HELD = {X, D};
|
||||
const SBindEventContext CONTEXT{
|
||||
.heldKeys = HELD,
|
||||
.trigger = D,
|
||||
.modifiersNow = HL_MODIFIER_META,
|
||||
};
|
||||
const std::array CANDIDATES = {
|
||||
SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)},
|
||||
SBindMatchCandidate{LONG, LONG->matches(CONTEXT)},
|
||||
};
|
||||
const std::array REVERSED_CANDIDATES = {
|
||||
SBindMatchCandidate{LONG, LONG->matches(CONTEXT)},
|
||||
SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)},
|
||||
};
|
||||
|
||||
for (const auto& candidates : {CANDIDATES, REVERSED_CANDIDATES}) {
|
||||
const auto RESOLUTION = resolveChordMatches(candidates, CONTEXT);
|
||||
EXPECT_TRUE(RESOLUTION.immediate.empty());
|
||||
ASSERT_EQ(RESOLUTION.deferred.size(), 1);
|
||||
EXPECT_EQ(RESOLUTION.deferred.front(), SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(KeybindsMatchResolver, SidedModifierDoesNotIncreaseChordLength) {
|
||||
|
||||
Reference in New Issue
Block a user