Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -615,10 +615,8 @@ namespace Hooks
{
auto menu = globals::menu;
if (a_msg == WM_KILLFOCUS && menu->initialized) {
// Only call our OnFocusLost() method which handles everything properly
menu->OnFocusLost();
auto& io = ImGui::GetIO();
io.ClearInputKeys();
io.ClearEventsQueue();
}
return func(a_hwnd, a_msg, a_wParam, a_lParam);
}
Expand Down
62 changes: 62 additions & 0 deletions src/Menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2129,6 +2129,9 @@ void Menu::ProcessInputEventQueue()
std::unique_lock<std::shared_mutex> mutex(_inputEventMutex);
ImGuiIO& io = ImGui::GetIO();

// Check if we're in the focus loss debounce period
bool shouldProcessEvents = ShouldProcessKeyEvents();

for (auto& event : _keyEventQueue) {
if (event.eventType == RE::INPUT_EVENT_TYPE::kChar) {
io.AddInputCharacter(event.keyCode);
Expand All @@ -2151,6 +2154,22 @@ void Menu::ProcessInputEventQueue()
logger::trace("Detected key code {} ({})", event.keyCode, key);
if (key == event.keyCode)
key = MapVirtualKeyEx(event.keyCode, MAPVK_VSC_TO_VK_EX, GetKeyboardLayout(0));

// Track key press/release states for reliable cleanup on focus loss
if (event.IsPressed()) {
pressedKeys.insert(key);
} else {
pressedKeys.erase(key);
}

// Skip processing key actions during debounce window to prevent false triggers
if (!shouldProcessEvents) {
logger::trace("Skipping key event processing during focus loss debounce window");
// Still send to ImGui for UI consistency, but skip our application logic
io.AddKeyEvent(VirtualKeyToImGuiKey(key), event.IsPressed());
continue;
}

if (!event.IsPressed()) {
struct HotkeyAction
{
Expand Down Expand Up @@ -2200,6 +2219,7 @@ void Menu::ProcessInputEventQueue()
}
}

// Always send key events to ImGui for UI consistency
io.AddKeyEvent(VirtualKeyToImGuiKey(key), event.IsPressed());

if (key == VK_LCONTROL || key == VK_RCONTROL)
Expand All @@ -2224,6 +2244,48 @@ void Menu::OnFocusLost()
{
std::unique_lock<std::shared_mutex> mutex(_inputEventMutex);
_keyEventQueue.clear();

// Reset all key states to prevent stuck keys after Alt+Tab or focus loss
ImGuiIO& io = ImGui::GetIO();

// Log the number of tracked keys before clearing
size_t keyCount = pressedKeys.size();

// Reset all tracked pressed keys by sending key-up events
for (uint32_t key : pressedKeys) {
ImGuiKey imguiKey = VirtualKeyToImGuiKey(key);
if (imguiKey != ImGuiKey_None) {
io.AddKeyEvent(imguiKey, false); // Send key-up event
}
}
pressedKeys.clear();

// Reset modifier keys specifically (common culprits for sticking)
io.AddKeyEvent(ImGuiMod_Ctrl, false);
io.AddKeyEvent(ImGuiMod_Shift, false);
io.AddKeyEvent(ImGuiMod_Alt, false);

// Reset common problematic keys
io.AddKeyEvent(ImGuiKey_Tab, false);
io.AddKeyEvent(ImGuiKey_Escape, false);
io.AddKeyEvent(ImGuiKey_Space, false);
io.AddKeyEvent(ImGuiKey_Enter, false);

// Clear ImGui's internal navigation/active state to prevent infinite iteration
// This ensures that any active widget (input fields, dropdowns, etc.) loses focus
// and prevents navigation keys from getting stuck in infinite loops
ImGui::ClearActiveID();

// Record the focus loss time for debouncing
lastFocusLossTime = std::chrono::steady_clock::now();

logger::trace("Focus lost - cleared all key states ({} tracked keys), ImGui active ID, and event queue", keyCount);
}
Comment on lines +2248 to +2283

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing mouse button / wheel reset

OnFocusLost() releases keyboard keys but does not clear mouse buttons/wheel states.
If the user Alt-Tabs while holding a mouse button, ImGui may think it remains pressed.
Add:

for (int btn = 0; btn < ImGuiMouseButton_COUNT; ++btn)
    io.AddMouseButtonEvent(btn, false);
io.AddMouseWheelEvent(0,0);
🤖 Prompt for AI Agents
In src/Menu.cpp around lines 2248 to 2283, the OnFocusLost() function resets
keyboard keys but does not clear mouse button or wheel states, which can cause
ImGui to think mouse buttons remain pressed after focus loss. To fix this, add a
loop to send mouse button release events for all mouse buttons using
io.AddMouseButtonEvent with false, and reset the mouse wheel state by calling
io.AddMouseWheelEvent with zero values. Place this code after resetting keyboard
keys and before clearing ImGui's active ID.


bool Menu::ShouldProcessKeyEvents() const
{
auto now = std::chrono::steady_clock::now();
return (now - lastFocusLossTime) >= FOCUS_LOSS_DEBOUNCE_MS;
}

void Menu::ProcessInputEvents(RE::InputEvent* const* a_events)
Expand Down
8 changes: 8 additions & 0 deletions src/Menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class Menu
bool ShouldSwallowInput();
void OnFocusLost();

// Helper method to check if key events should be processed (not in debounce window)
bool ShouldProcessKeyEvents() const;

struct ThemeSettings
{
float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential
Expand Down Expand Up @@ -244,6 +247,11 @@ class Menu

std::chrono::steady_clock::time_point lastTestSwitch = high_resolution_clock::now(); // Time of last test switch

// Key debouncing and focus loss handling
std::unordered_set<uint32_t> pressedKeys; // Track currently pressed keys
std::chrono::steady_clock::time_point lastFocusLossTime; // Time when focus was lost
static constexpr auto FOCUS_LOSS_DEBOUNCE_MS = std::chrono::milliseconds(100); // Debounce period after focus loss

Menu() = default;
void SetupImGuiStyle() const;
const char* KeyIdToString(uint32_t key);
Expand Down