Skip to content

Commit

Permalink
Detect Shortcut: Hold Esc/Enter to Cancel/Accept
Browse files Browse the repository at this point in the history
Bypass shorcut/single key remapping by holding the navigation keys
  • Loading branch information
Tomas Raies committed Apr 15, 2020
1 parent 95eb17b commit e9e3a3b
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 20 deletions.
2 changes: 1 addition & 1 deletion PowerToys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ Global
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4981CCD1-4CD9-4A49-B240-00AA46493FF8}
{B25AC7A5-FB9F-4789-B392-D5C85E948670} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
{51920F1F-C28C-4ADF-8660-4238766796C2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
{0E072714-D127-460B-AFAD-B4C40B412798} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
Expand Down
149 changes: 149 additions & 0 deletions src/modules/keyboardmanager/common/KeyDelay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include "pch.h"
#include "KeyDelay.h"

#define LONG_PRESS_DELAY_MILLIS 900
#define ON_HOLD_WAIT_TIMEOUT_MILLIS 50

KeyDelay::~KeyDelay()
{
_quit = true;
_cv.notify_all();
_delayThread.join();
}

void KeyDelay::KeyEvent(LowlevelKeyboardEvent* ev)
{
std::lock_guard guard(_queueMutex);
_queue.push({ ev->lParam->time, ev->wParam });
_cv.notify_all();
}

KeyTimedEvent KeyDelay::NextEvent()
{
auto ev = _queue.front();
_queue.pop();
return ev;
}

bool KeyDelay::CheckIfMillisHaveElapsed(DWORD first, DWORD last, DWORD duration)
{
if (first < last && first <= first + duration)
{
return first + duration < last;
}
else
{
first += ULONG_MAX / 2;
last += ULONG_MAX / 2;
return first + duration < last;
}
}

bool KeyDelay::HasNextEvent()
{
return !_queue.empty();
}

bool KeyDelay::HandleRelease()
{
while (HasNextEvent())
{
auto ev = NextEvent();
switch (ev.message)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
_state = KeyDelayState::ON_HOLD;
_initialHoldKeyDown = ev.time;
return false;
case WM_KEYUP:
case WM_SYSKEYUP:
break;
}
}

return true;
}

bool KeyDelay::HandleOnHold(std::unique_lock<std::mutex>& cvLock)
{
while (HasNextEvent())
{
auto ev = NextEvent();
switch (ev.message)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
break;
case WM_KEYUP:
case WM_SYSKEYUP:
if (CheckIfMillisHaveElapsed(_initialHoldKeyDown, ev.time, LONG_PRESS_DELAY_MILLIS))
{
_onLongPress(_key);
}
else
{
_onShortPress(_key);
}
_state = KeyDelayState::RELEASED;
return false;
}
}

if (CheckIfMillisHaveElapsed(_initialHoldKeyDown, GetTickCount(), LONG_PRESS_DELAY_MILLIS))
{
_onLongPress(_key);
_state = KeyDelayState::ON_HOLD_TIMEOUT;
}
else
{
_cv.wait_for(cvLock, std::chrono::milliseconds(ON_HOLD_WAIT_TIMEOUT_MILLIS));
}
return false;
}

bool KeyDelay::HandleOnHoldTimeout()
{
while (HasNextEvent())
{
auto ev = NextEvent();
switch (ev.message)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
_state = KeyDelayState::RELEASED;
return false;
}
}

return true;
}

void KeyDelay::DelayThread()
{
std::unique_lock<std::mutex> qLock(_queueMutex);
bool shouldWait = true;

while (!_quit)
{
if (shouldWait)
{
_cv.wait(qLock);
}

switch (_state)
{
case KeyDelayState::RELEASED:
shouldWait = HandleRelease();
break;
case KeyDelayState::ON_HOLD:
shouldWait = HandleOnHold(qLock);
break;
case KeyDelayState::ON_HOLD_TIMEOUT:
shouldWait = HandleOnHoldTimeout();
break;
}
}
}
63 changes: 63 additions & 0 deletions src/modules/keyboardmanager/common/KeyDelay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#pragma once
#include <interface/lowlevel_keyboard_event_data.h>
#include <functional>
#include <thread>
#include <queue>
#include <mutex>
#include <chrono>

enum class KeyDelayState
{
RELEASED,
ON_HOLD,
ON_HOLD_TIMEOUT,
};

struct KeyTimedEvent
{
DWORD time;
WPARAM message;
};

class KeyDelay
{
public:
KeyDelay(
DWORD key,
std::function<void(DWORD)> onShortPress,
std::function<void(DWORD)> onLongPress
) :
_quit(false),
_state(KeyDelayState::RELEASED),
_initialHoldKeyDown(0),
_key(key),
_onLongPress(onLongPress),
_onShortPress(onShortPress),
_delayThread(&KeyDelay::DelayThread, this)
{};

void KeyEvent(LowlevelKeyboardEvent* ev);
~KeyDelay();

private:
void DelayThread();
bool HandleRelease();
bool HandleOnHold(std::unique_lock<std::mutex>& cvLock);
bool HandleOnHoldTimeout();
KeyTimedEvent NextEvent();
bool HasNextEvent();
bool CheckIfMillisHaveElapsed(DWORD first, DWORD last, DWORD duration);

std::thread _delayThread;
bool _quit;
KeyDelayState _state;
std::function<void(DWORD)> _onLongPress;
std::function<void(DWORD)> _onShortPress;
std::queue<KeyTimedEvent> _queue;
std::mutex _queueMutex;
std::condition_variable _cv;
DWORD _initialHoldKeyDown;
DWORD _key;
};


Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<ItemGroup>
<ClCompile Include="Helpers.cpp" />
<ClCompile Include="KeyboardManagerState.cpp" />
<ClCompile Include="KeyDelay.cpp" />
<ClCompile Include="LayoutMap.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
Expand All @@ -100,6 +101,7 @@
<ItemGroup>
<ClInclude Include="Helpers.h" />
<ClInclude Include="KeyboardManagerState.h" />
<ClInclude Include="KeyDelay.h" />
<ClInclude Include="LayoutMap.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="RemapShortcut.h" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
<ClCompile Include="RemapShortcut.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeyDelay.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="KeyboardManagerState.h">
Expand All @@ -53,5 +56,8 @@
<ClInclude Include="RemapShortcut.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KeyDelay.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
82 changes: 65 additions & 17 deletions src/modules/keyboardmanager/common/KeyboardManagerState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

// Constructor
KeyboardManagerState::KeyboardManagerState() :
uiState(KeyboardManagerUIState::Deactivated), currentUIWindow(nullptr), currentShortcutUI(nullptr), currentSingleKeyUI(nullptr), detectedRemapKey(NULL)
uiState(KeyboardManagerUIState::Deactivated),
currentUIWindow(nullptr),
currentShortcutUI(nullptr),
currentSingleKeyUI(nullptr),
detectedRemapKey(NULL)
{
}

Expand Down Expand Up @@ -213,6 +217,29 @@ DWORD KeyboardManagerState::GetDetectedSingleRemapKey()
return detectedRemapKey;
}

void KeyboardManagerState::SelectDetectedRemapKey(DWORD key)
{
std::lock_guard<std::mutex> guard(detectedRemapKey_mutex);
detectedRemapKey = key;
UpdateDetectSingleKeyRemapUI();
return;
}

void KeyboardManagerState::SelectDetectedShortcut(DWORD key)
{
// Set the new key and store if a change occured
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
bool updateUI = detectedShortcut.SetKey(key);
lock.unlock();

if (updateUI)
{
// Update the UI. This function is called here because it should store the set of keys pressed till the last key which was pressed down.
UpdateDetectShortcutUI();
}
return;
}

// Function which can be used in HandleKeyboardHookEvent before the single key remap event to use the UI and suppress events while the remap window is active.
bool KeyboardManagerState::DetectSingleRemapKeyUIBackend(LowlevelKeyboardEvent* data)
{
Expand All @@ -222,13 +249,9 @@ bool KeyboardManagerState::DetectSingleRemapKeyUIBackend(LowlevelKeyboardEvent*
// detect the key if it is pressed down
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
std::unique_lock<std::mutex> detectedRemapKey_lock(detectedRemapKey_mutex);
detectedRemapKey = data->lParam->vkCode;
detectedRemapKey_lock.unlock();

UpdateDetectSingleKeyRemapUI();
SelectDetectedRemapKey(data->lParam->vkCode);
}

// Suppress the keyboard event
return true;
}
Expand All @@ -245,16 +268,7 @@ bool KeyboardManagerState::DetectShortcutUIBackend(LowlevelKeyboardEvent* data)
// Add the key if it is pressed down
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
// Set the new key and store if a change occured
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
bool updateUI = detectedShortcut.SetKey(data->lParam->vkCode);
lock.unlock();

if (updateUI)
{
// Update the UI. This function is called here because it should store the set of keys pressed till the last key which was pressed down.
UpdateDetectShortcutUI();
}
SelectDetectedShortcut(data->lParam->vkCode);
}
// Remove the key if it has been released
else if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP)
Expand All @@ -278,4 +292,38 @@ bool KeyboardManagerState::DetectShortcutUIBackend(LowlevelKeyboardEvent* data)
}

return false;
}

void KeyboardManagerState::RegisterKeyDelay(DWORD key, std::function<void(DWORD)> onShortPress, std::function<void(DWORD)> onLongPress)
{
std::lock_guard l(keyDelays_mutex);

if (keyDelays.find(key) != keyDelays.end())
{
throw std::invalid_argument("This key was already registered.");
}
keyDelays[key] = std::make_unique<KeyDelay>(key, onShortPress, onLongPress);
}

void KeyboardManagerState::UnregisterKeyDelay(DWORD key)
{
std::lock_guard l(keyDelays_mutex);

auto deleted = keyDelays.erase(key);
if (deleted == 0) {
throw std::invalid_argument("The key was not previously registered.");
}
}

bool KeyboardManagerState::HandleKeyDelayEvent(LowlevelKeyboardEvent* ev)
{
std::lock_guard l(keyDelays_mutex);

if (keyDelays.find(ev->lParam->vkCode) == keyDelays.end())
{
return false;
}

keyDelays[ev->lParam->vkCode]->KeyEvent(ev);
return true;
}
Loading

0 comments on commit e9e3a3b

Please sign in to comment.