Detect Shortcut: Hold Esc/Enter to Cancel/Accept (#2135)

* Detect Shortcut: Hold Esc/Enter to Discard/Apply changes

Bypass shorcut/single key remapping by holding the navigation keys
This commit is contained in:
Tomas Agustin Raies
2020-04-16 09:16:48 -07:00
committed by GitHub
parent 5d9b71b038
commit c37884bdb7
10 changed files with 594 additions and 38 deletions

View File

@@ -0,0 +1,165 @@
#include "pch.h"
#include "KeyDelay.h"
KeyDelay::~KeyDelay()
{
std::unique_lock<std::mutex> l(_queueMutex);
_quit = true;
_cv.notify_all();
l.unlock();
_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))
{
if (_onLongPressDetected != nullptr)
{
_onLongPressDetected(_key);
}
if (_onLongPressReleased != nullptr)
{
_onLongPressReleased(_key);
}
}
else
{
if (_onShortPress != nullptr)
{
_onShortPress(_key);
}
}
_state = KeyDelayState::RELEASED;
return false;
}
}
if (CheckIfMillisHaveElapsed(_initialHoldKeyDown, GetTickCount(), LONG_PRESS_DELAY_MILLIS))
{
if (_onLongPressDetected != nullptr)
{
_onLongPressDetected(_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:
break;
case WM_KEYUP:
case WM_SYSKEYUP:
if (_onLongPressReleased != nullptr)
{
_onLongPressReleased(_key);
}
_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;
}
}
}

View File

@@ -0,0 +1,91 @@
#pragma once
#include <interface/lowlevel_keyboard_event_data.h>
#include <functional>
#include <thread>
#include <queue>
#include <mutex>
// Available states for the KeyDelay state machine.
enum class KeyDelayState
{
RELEASED,
ON_HOLD,
ON_HOLD_TIMEOUT,
};
// Virtual key + timestamp (in millis since Windows startup)
struct KeyTimedEvent
{
DWORD time;
WPARAM message;
};
// Handles delayed key inputs.
// Implemented as a state machine running on its own thread.
// Thread stops on destruction.
class KeyDelay
{
public:
KeyDelay(
DWORD key,
std::function<void(DWORD)> onShortPress,
std::function<void(DWORD)> onLongPressDetected,
std::function<void(DWORD)> onLongPressReleased) :
_quit(false),
_state(KeyDelayState::RELEASED),
_initialHoldKeyDown(0),
_key(key),
_onShortPress(onShortPress),
_onLongPressDetected(onLongPressDetected),
_onLongPressReleased(onLongPressReleased),
_delayThread(&KeyDelay::DelayThread, this){};
// Enque new KeyTimedEvent and notify the condition variable.
void KeyEvent(LowlevelKeyboardEvent* ev);
~KeyDelay();
private:
// Runs the state machine, waits if there is no events to process.
// Checks for _quit condition.
void DelayThread();
// Manage state transitions and trigger callbacks on certain events.
// Returns whether or not the thread should wait on new events.
bool HandleRelease();
bool HandleOnHold(std::unique_lock<std::mutex>& cvLock);
bool HandleOnHoldTimeout();
// Get next key event in queue.
KeyTimedEvent NextEvent();
bool HasNextEvent();
// Check if <duration> milliseconds passed since <first> millisecond.
// Also checks for overflow conditions.
bool CheckIfMillisHaveElapsed(DWORD first, DWORD last, DWORD duration);
std::thread _delayThread;
bool _quit;
KeyDelayState _state;
// Callback functions, the key provided in the constructor is passed as an argument.
std::function<void(DWORD)> _onLongPressDetected;
std::function<void(DWORD)> _onLongPressReleased;
std::function<void(DWORD)> _onShortPress;
// Queue holding key events that are not processed yet. Should be kept synchronized
// using _queueMutex
std::queue<KeyTimedEvent> _queue;
std::mutex _queueMutex;
// DelayThread waits on this condition variable when there is no events to process.
std::condition_variable _cv;
// Keeps track of the time at which the initial KEY_DOWN event happened.
DWORD _initialHoldKeyDown;
// Virtual Key provided in the constructor. Passed to callback functions.
DWORD _key;
static const DWORD LONG_PRESS_DELAY_MILLIS = 900;
static const DWORD ON_HOLD_WAIT_TIMEOUT_MILLIS = 50;
};

View File

@@ -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>
@@ -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" />

View File

@@ -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">
@@ -53,5 +56,8 @@
<ClInclude Include="RemapShortcut.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KeyDelay.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -132,7 +132,6 @@ void KeyboardManagerState::ConfigureDetectSingleKeyRemapUI(const StackPanel& tex
currentSingleKeyUI = textBlock;
}
void KeyboardManagerState::AddKeyToLayout(const StackPanel& panel, const hstring& key)
{
// Textblock to display the detected key
@@ -140,7 +139,7 @@ void KeyboardManagerState::AddKeyToLayout(const StackPanel& panel, const hstring
Border border;
border.Padding({ 20, 10, 20, 10 });
border.Margin({0, 0, 10, 0 });
border.Margin({ 0, 0, 10, 0 });
border.Background(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::LightGray() });
remapKey.Foreground(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::Black() });
remapKey.FontSize(20);
@@ -160,18 +159,17 @@ void KeyboardManagerState::UpdateDetectShortcutUI()
return;
}
std::unique_lock<std::mutex> detectedShortcut_lock(detectedShortcut_mutex);
std::unique_lock<std::mutex> currentShortcut_lock(currentShortcut_mutex);
// Save the latest displayed shortcut
currentShortcut = detectedShortcut;
auto detectedShortcutCopy = detectedShortcut;
currentShortcut_lock.unlock();
detectedShortcut_lock.unlock();
// Since this function is invoked from the back-end thread, in order to update the UI the dispatcher must be used.
currentShortcutUI.Dispatcher().RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, [this]() {
std::vector<hstring> shortcut = detectedShortcut.GetKeyVector(keyboardMap);
currentShortcutUI.Dispatcher().RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, [this, detectedShortcutCopy]() {
std::vector<hstring> shortcut = detectedShortcutCopy.GetKeyVector(keyboardMap);
currentShortcutUI.Children().Clear();
for (auto& key : shortcut)
{
@@ -213,20 +211,49 @@ 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;
}
void KeyboardManagerState::ResetDetectedShortcutKey(DWORD key)
{
std::lock_guard<std::mutex> lock(detectedShortcut_mutex);
detectedShortcut.ResetKey(key);
}
// 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)
{
// Check if the detect key UI window has been activated
if (CheckUIState(KeyboardManagerUIState::DetectSingleKeyRemapWindowActivated))
{
if (HandleKeyDelayEvent(data))
{
return true;
}
// 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
@@ -242,25 +269,20 @@ bool KeyboardManagerState::DetectShortcutUIBackend(LowlevelKeyboardEvent* data)
// Check if the detect shortcut UI window has been activated
if (CheckUIState(KeyboardManagerUIState::DetectShortcutWindowActivated))
{
if (HandleKeyDelayEvent(data))
{
return true;
}
// 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)
{
std::lock_guard<std::mutex> lock(detectedShortcut_mutex);
detectedShortcut.ResetKey(data->lParam->vkCode);
ResetDetectedShortcutKey(data->lParam->vkCode);
}
// Suppress the keyboard event
@@ -278,4 +300,48 @@ bool KeyboardManagerState::DetectShortcutUIBackend(LowlevelKeyboardEvent* data)
}
return false;
}
void KeyboardManagerState::RegisterKeyDelay(
DWORD key,
std::function<void(DWORD)> onShortPress,
std::function<void(DWORD)> onLongPressDetected,
std::function<void(DWORD)> onLongPressReleased)
{
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, onLongPressDetected, onLongPressReleased);
}
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)
{
if (currentUIWindow != GetForegroundWindow())
{
return false;
}
std::lock_guard l(keyDelays_mutex);
if (keyDelays.find(ev->lParam->vkCode) == keyDelays.end())
{
return false;
}
keyDelays[ev->lParam->vkCode]->KeyEvent(ev);
return true;
}

View File

@@ -3,6 +3,7 @@
#include "LayoutMap.h"
#include "Shortcut.h"
#include "RemapShortcut.h"
#include "KeyDelay.h"
#include <interface/lowlevel_keyboard_event_data.h>
#include <mutex>
#include <winrt/Windows.UI.Xaml.Controls.h>
@@ -51,8 +52,13 @@ private:
StackPanel currentShortcutUI;
std::mutex currentShortcutUI_mutex;
// Registered KeyDelay objects, used to notify delayed key events.
std::map<DWORD, std::unique_ptr<KeyDelay>> keyDelays;
std::mutex keyDelays_mutex;
// Display a key by appending a border Control as a child of the panel.
void AddKeyToLayout(const StackPanel& panel, const winrt::hstring& key);
public:
// The map members and their mutexes are left as public since the maps are used extensively in dllmain.cpp.
// Maps which store the remappings for each of the features. The bool fields should be initalised to false. They are used to check the current state of the shortcut (i.e is that particular shortcut currently pressed down or not).
@@ -125,4 +131,30 @@ public:
// Function which can be used in HandleKeyboardHookEvent before the os level shortcut remap event to use the UI and suppress events while the remap window is active.
bool DetectShortcutUIBackend(LowlevelKeyboardEvent* data);
// Add a KeyDelay object to get delayed key presses events for a given virtual key
// NOTE: this will throw an exception if a virtual key is registered twice.
// NOTE*: the virtual key should represent the original, unmapped virtual key.
void RegisterKeyDelay(
DWORD key,
std::function<void(DWORD)> onShortPress,
std::function<void(DWORD)> onLongPressDetected,
std::function<void(DWORD)> onLongPressReleased);
// Remove a KeyDelay.
// NOTE: this method will throw if the virtual key is not registered beforehand.
// NOTE*: the virtual key should represent the original, unmapped virtual key.
void UnregisterKeyDelay(DWORD key);
// Handle a key event, for a delayed key.
bool HandleKeyDelayEvent(LowlevelKeyboardEvent* ev);
// Update the currently selected single key remap
void SelectDetectedRemapKey(DWORD key);
// Update the currently selected shortcut.
void SelectDetectedShortcut(DWORD key);
// Reset the shortcut (backend) state after releasing a key.
void ResetDetectedShortcutKey(DWORD key);
};

View File

@@ -411,7 +411,7 @@ winrt::hstring Shortcut::ToHstring(LayoutMap& keyboardMap)
}
}
std::vector<winrt::hstring> Shortcut::GetKeyVector(LayoutMap& keyboardMap)
std::vector<winrt::hstring> Shortcut::GetKeyVector(LayoutMap& keyboardMap) const
{
std::vector<winrt::hstring> keys;
if (winKey != ModifierKey::Disabled)

View File

@@ -140,7 +140,7 @@ public:
winrt::hstring ToHstring(LayoutMap& keyboardMap);
// Function to return a vector of hstring for each key, in the same order as ToHstring()
std::vector<winrt::hstring> GetKeyVector(LayoutMap& keyboardMap);
std::vector<winrt::hstring> GetKeyVector(LayoutMap& keyboardMap) const;
// Function to check if all the modifiers in the shortcut have been pressed down
bool CheckModifiersKeyboardState() const;

View File

@@ -72,15 +72,31 @@ void ShortcutControl::createDetectShortcutWindow(IInspectable const& sender, Xam
// ContentDialog requires manually setting the XamlRoot (https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.contentdialog#contentdialog-in-appwindow-or-xaml-islands)
detectShortcutBox.XamlRoot(xamlRoot);
detectShortcutBox.Title(box_value(L"Press the keys in shortcut:"));
detectShortcutBox.PrimaryButtonText(to_hstring(L"OK"));
detectShortcutBox.IsPrimaryButtonEnabled(false);
detectShortcutBox.IsSecondaryButtonEnabled(false);
detectShortcutBox.CloseButtonText(to_hstring(L"Cancel"));
// Get the linked text block for the "Type shortcut" button that was clicked
TextBlock linkedShortcutText = getSiblingElement(sender).as<TextBlock>();
// OK button
detectShortcutBox.PrimaryButtonClick([=, &shortcutRemapBuffer, &keyboardManagerState](Windows::UI::Xaml::Controls::ContentDialog const& sender, ContentDialogButtonClickEventArgs const&) {
auto unregisterKeys = [&keyboardManagerState]() {
std::thread t1(&KeyboardManagerState::UnregisterKeyDelay, &keyboardManagerState, VK_ESCAPE);
std::thread t2(&KeyboardManagerState::UnregisterKeyDelay, &keyboardManagerState, VK_RETURN);
t1.detach();
t2.detach();
};
auto selectDetectedShortcutAndResetKeys = [&keyboardManagerState](DWORD key) {
keyboardManagerState.SelectDetectedShortcut(key);
keyboardManagerState.ResetDetectedShortcutKey(key);
};
auto onAccept = [linkedShortcutText,
detectShortcutBox,
&keyboardManagerState,
&shortcutRemapBuffer,
unregisterKeys,
rowIndex,
colIndex] {
// Save the detected shortcut in the linked text block
Shortcut detectedShortcutKeys = keyboardManagerState.GetDetectedShortcut();
@@ -92,14 +108,71 @@ void ShortcutControl::createDetectShortcutWindow(IInspectable const& sender, Xam
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
unregisterKeys();
detectShortcutBox.Hide();
};
TextBlock primaryButtonText;
primaryButtonText.Text(to_hstring(L"OK"));
Button primaryButton;
primaryButton.HorizontalAlignment(HorizontalAlignment::Stretch);
primaryButton.Margin({ 2, 2, 2, 2 });
primaryButton.Content(primaryButtonText);
// OK button
primaryButton.Click([onAccept](IInspectable const& sender, RoutedEventArgs const&) {
onAccept();
});
keyboardManagerState.RegisterKeyDelay(
VK_RETURN,
selectDetectedShortcutAndResetKeys,
[primaryButton, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[primaryButton] {
primaryButton.Background(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::DarkGray() });
});
},
[onAccept, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onAccept] {
onAccept();
});
});
TextBlock cancelButtonText;
cancelButtonText.Text(to_hstring(L"Cancel"));
Button cancelButton;
cancelButton.HorizontalAlignment(HorizontalAlignment::Stretch);
cancelButton.Margin({ 2, 2, 2, 2 });
cancelButton.Content(cancelButtonText);
// Cancel button
detectShortcutBox.CloseButtonClick([&keyboardManagerState](Windows::UI::Xaml::Controls::ContentDialog const& sender, ContentDialogButtonClickEventArgs const&) {
cancelButton.Click([detectShortcutBox, unregisterKeys, &keyboardManagerState](IInspectable const& sender, RoutedEventArgs const&) {
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
unregisterKeys();
detectShortcutBox.Hide();
});
keyboardManagerState.RegisterKeyDelay(
VK_ESCAPE,
selectDetectedShortcutAndResetKeys,
[&keyboardManagerState, detectShortcutBox, unregisterKeys](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[detectShortcutBox] {
detectShortcutBox.Hide();
});
keyboardManagerState.ResetUIState();
unregisterKeys();
},
nullptr);
// StackPanel parent for the displayed text in the dialog
Windows::UI::Xaml::Controls::StackPanel stackPanel;
detectShortcutBox.Content(stackPanel);
@@ -112,9 +185,36 @@ void ShortcutControl::createDetectShortcutWindow(IInspectable const& sender, Xam
// Target StackPanel to place the selected key
Windows::UI::Xaml::Controls::StackPanel keyStackPanel;
stackPanel.Children().Append(keyStackPanel);
keyStackPanel.Orientation(Orientation::Horizontal);
stackPanel.Children().Append(keyStackPanel);
TextBlock holdEscInfo;
holdEscInfo.Text(winrt::to_hstring("Hold Esc to discard"));
holdEscInfo.FontSize(12);
holdEscInfo.Margin({ 0, 20, 0, 0 });
stackPanel.Children().Append(holdEscInfo);
TextBlock holdEnterInfo;
holdEnterInfo.Text(winrt::to_hstring("Hold Enter to apply"));
holdEnterInfo.FontSize(12);
holdEnterInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdEnterInfo);
ColumnDefinition primaryButtonColumn;
ColumnDefinition cancelButtonColumn;
Grid buttonPanel;
buttonPanel.Margin({ 0, 20, 0, 0 });
buttonPanel.HorizontalAlignment(HorizontalAlignment::Stretch);
buttonPanel.ColumnDefinitions().Append(primaryButtonColumn);
buttonPanel.ColumnDefinitions().Append(cancelButtonColumn);
buttonPanel.SetColumn(primaryButton, 0);
buttonPanel.SetColumn(cancelButton, 1);
buttonPanel.Children().Append(primaryButton);
buttonPanel.Children().Append(cancelButton);
stackPanel.Children().Append(buttonPanel);
stackPanel.UpdateLayout();
// Configure the keyboardManagerState to store the UI information.

View File

@@ -68,21 +68,32 @@ void SingleKeyRemapControl::createDetectKeyWindow(IInspectable const& sender, Xa
{
// ContentDialog for detecting remap key. This is the parent UI element.
ContentDialog detectRemapKeyBox;
// TODO: Hardcoded light theme, since the app is not theme aware ATM.
detectRemapKeyBox.RequestedTheme(ElementTheme::Light);
// ContentDialog requires manually setting the XamlRoot (https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.contentdialog#contentdialog-in-appwindow-or-xaml-islands)
detectRemapKeyBox.XamlRoot(xamlRoot);
detectRemapKeyBox.Title(box_value(L"Press a key on selected keyboard:"));
detectRemapKeyBox.PrimaryButtonText(to_hstring(L"OK"));
detectRemapKeyBox.IsPrimaryButtonEnabled(false);
detectRemapKeyBox.IsSecondaryButtonEnabled(false);
detectRemapKeyBox.CloseButtonText(to_hstring(L"Cancel"));
// Get the linked text block for the "Type Key" button that was clicked
TextBlock linkedRemapText = getSiblingElement(sender).as<TextBlock>();
// OK button
detectRemapKeyBox.PrimaryButtonClick([=, &singleKeyRemapBuffer, &keyboardManagerState](Windows::UI::Xaml::Controls::ContentDialog const& sender, ContentDialogButtonClickEventArgs const&) {
auto unregisterKeys = [&keyboardManagerState]() {
std::thread t1(&KeyboardManagerState::UnregisterKeyDelay, &keyboardManagerState, VK_ESCAPE);
std::thread t2(&KeyboardManagerState::UnregisterKeyDelay, &keyboardManagerState, VK_RETURN);
t1.detach();
t2.detach();
};
auto onAccept = [linkedRemapText,
detectRemapKeyBox,
&keyboardManagerState,
&singleKeyRemapBuffer,
unregisterKeys,
rowIndex,
colIndex] {
// Save the detected key in the linked text block
DWORD detectedKey = keyboardManagerState.GetDetectedSingleRemapKey();
@@ -94,14 +105,69 @@ void SingleKeyRemapControl::createDetectKeyWindow(IInspectable const& sender, Xa
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
unregisterKeys();
detectRemapKeyBox.Hide();
};
TextBlock primaryButtonText;
primaryButtonText.Text(to_hstring(L"OK"));
Button primaryButton;
primaryButton.HorizontalAlignment(HorizontalAlignment::Stretch);
primaryButton.Margin({ 2, 2, 2, 2 });
primaryButton.Content(primaryButtonText);
primaryButton.Click([onAccept](IInspectable const& sender, RoutedEventArgs const&) {
onAccept();
});
keyboardManagerState.RegisterKeyDelay(
VK_RETURN,
std::bind(&KeyboardManagerState::SelectDetectedRemapKey, &keyboardManagerState, std::placeholders::_1),
[primaryButton, detectRemapKeyBox](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[primaryButton] {
primaryButton.Background(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::DarkGray() });
});
},
[onAccept, detectRemapKeyBox](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onAccept] {
onAccept();
});
});
TextBlock cancelButtonText;
cancelButtonText.Text(to_hstring(L"Cancel"));
Button cancelButton;
cancelButton.HorizontalAlignment(HorizontalAlignment::Stretch);
cancelButton.Margin({ 2, 2, 2, 2 });
cancelButton.Content(cancelButtonText);
// Cancel button
detectRemapKeyBox.CloseButtonClick([&keyboardManagerState](Windows::UI::Xaml::Controls::ContentDialog const& sender, ContentDialogButtonClickEventArgs const&) {
cancelButton.Click([detectRemapKeyBox, unregisterKeys, &keyboardManagerState](IInspectable const& sender, RoutedEventArgs const&) {
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
unregisterKeys();
detectRemapKeyBox.Hide();
});
keyboardManagerState.RegisterKeyDelay(
VK_ESCAPE,
std::bind(&KeyboardManagerState::SelectDetectedRemapKey, &keyboardManagerState, std::placeholders::_1),
[&keyboardManagerState, detectRemapKeyBox, unregisterKeys](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[detectRemapKeyBox] {
detectRemapKeyBox.Hide();
});
keyboardManagerState.ResetUIState();
unregisterKeys();
},
nullptr);
// StackPanel parent for the displayed text in the dialog
Windows::UI::Xaml::Controls::StackPanel stackPanel;
detectRemapKeyBox.Content(stackPanel);
@@ -114,8 +180,36 @@ void SingleKeyRemapControl::createDetectKeyWindow(IInspectable const& sender, Xa
// Target StackPanel to place the selected key
Windows::UI::Xaml::Controls::StackPanel keyStackPanel;
stackPanel.Children().Append(keyStackPanel);
keyStackPanel.Orientation(Orientation::Horizontal);
stackPanel.Children().Append(keyStackPanel);
TextBlock holdEscInfo;
holdEscInfo.Text(winrt::to_hstring("Hold Esc to discard"));
holdEscInfo.FontSize(12);
holdEscInfo.Margin({ 0, 20, 0, 0 });
stackPanel.Children().Append(holdEscInfo);
TextBlock holdEnterInfo;
holdEnterInfo.Text(winrt::to_hstring("Hold Enter to apply"));
holdEnterInfo.FontSize(12);
holdEnterInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdEnterInfo);
ColumnDefinition primaryButtonColumn;
ColumnDefinition cancelButtonColumn;
Grid buttonPanel;
buttonPanel.Margin({ 0, 20, 0, 0 });
buttonPanel.HorizontalAlignment(HorizontalAlignment::Stretch);
buttonPanel.ColumnDefinitions().Append(primaryButtonColumn);
buttonPanel.ColumnDefinitions().Append(cancelButtonColumn);
buttonPanel.SetColumn(primaryButton, 0);
buttonPanel.SetColumn(cancelButton, 1);
buttonPanel.Children().Append(primaryButton);
buttonPanel.Children().Append(cancelButton);
stackPanel.Children().Append(buttonPanel);
stackPanel.UpdateLayout();
// Configure the keyboardManagerState to store the UI information.