diff --git a/src/modules/keyboardmanager/common/KeyDelay.cpp b/src/modules/keyboardmanager/common/KeyDelay.cpp new file mode 100644 index 0000000000..0e41e7895f --- /dev/null +++ b/src/modules/keyboardmanager/common/KeyDelay.cpp @@ -0,0 +1,165 @@ +#include "pch.h" +#include "KeyDelay.h" + +KeyDelay::~KeyDelay() +{ + std::unique_lock 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& 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 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; + } + } +} \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/KeyDelay.h b/src/modules/keyboardmanager/common/KeyDelay.h new file mode 100644 index 0000000000..164219e478 --- /dev/null +++ b/src/modules/keyboardmanager/common/KeyDelay.h @@ -0,0 +1,91 @@ +#pragma once +#include +#include +#include +#include +#include + +// 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 onShortPress, + std::function onLongPressDetected, + std::function 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& cvLock); + bool HandleOnHoldTimeout(); + + // Get next key event in queue. + KeyTimedEvent NextEvent(); + bool HasNextEvent(); + + // Check if milliseconds passed since 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 _onLongPressDetected; + std::function _onLongPressReleased; + std::function _onShortPress; + + // Queue holding key events that are not processed yet. Should be kept synchronized + // using _queueMutex + std::queue _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; +}; diff --git a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj index 9fc11fdcee..aa0affb8cf 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj +++ b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj @@ -89,6 +89,7 @@ + Create @@ -100,6 +101,7 @@ + diff --git a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters index 8451c15957..9cf7199d16 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters +++ b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters @@ -33,6 +33,9 @@ Source Files + + Source Files + @@ -53,5 +56,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/KeyboardManagerState.cpp b/src/modules/keyboardmanager/common/KeyboardManagerState.cpp index 0c3576a4fc..4aff6ccda7 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerState.cpp +++ b/src/modules/keyboardmanager/common/KeyboardManagerState.cpp @@ -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 detectedShortcut_lock(detectedShortcut_mutex); std::unique_lock 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 shortcut = detectedShortcut.GetKeyVector(keyboardMap); + currentShortcutUI.Dispatcher().RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, [this, detectedShortcutCopy]() { + std::vector 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 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 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 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 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 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 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 onShortPress, + std::function onLongPressDetected, + std::function 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(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; } \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/KeyboardManagerState.h b/src/modules/keyboardmanager/common/KeyboardManagerState.h index 62d90fb437..8280652cf0 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerState.h +++ b/src/modules/keyboardmanager/common/KeyboardManagerState.h @@ -3,6 +3,7 @@ #include "LayoutMap.h" #include "Shortcut.h" #include "RemapShortcut.h" +#include "KeyDelay.h" #include #include #include @@ -51,8 +52,13 @@ private: StackPanel currentShortcutUI; std::mutex currentShortcutUI_mutex; + // Registered KeyDelay objects, used to notify delayed key events. + std::map> 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 onShortPress, + std::function onLongPressDetected, + std::function 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); }; \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/Shortcut.cpp b/src/modules/keyboardmanager/common/Shortcut.cpp index 3966edffa2..b35c3bf984 100644 --- a/src/modules/keyboardmanager/common/Shortcut.cpp +++ b/src/modules/keyboardmanager/common/Shortcut.cpp @@ -411,7 +411,7 @@ winrt::hstring Shortcut::ToHstring(LayoutMap& keyboardMap) } } -std::vector Shortcut::GetKeyVector(LayoutMap& keyboardMap) +std::vector Shortcut::GetKeyVector(LayoutMap& keyboardMap) const { std::vector keys; if (winKey != ModifierKey::Disabled) diff --git a/src/modules/keyboardmanager/common/Shortcut.h b/src/modules/keyboardmanager/common/Shortcut.h index 304e5d0154..960c319f33 100644 --- a/src/modules/keyboardmanager/common/Shortcut.h +++ b/src/modules/keyboardmanager/common/Shortcut.h @@ -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 GetKeyVector(LayoutMap& keyboardMap); + std::vector GetKeyVector(LayoutMap& keyboardMap) const; // Function to check if all the modifiers in the shortcut have been pressed down bool CheckModifiersKeyboardState() const; diff --git a/src/modules/keyboardmanager/ui/ShortcutControl.cpp b/src/modules/keyboardmanager/ui/ShortcutControl.cpp index a8df1ef9ac..8a9e2e94af 100644 --- a/src/modules/keyboardmanager/ui/ShortcutControl.cpp +++ b/src/modules/keyboardmanager/ui/ShortcutControl.cpp @@ -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(); - // 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. diff --git a/src/modules/keyboardmanager/ui/SingleKeyRemapControl.cpp b/src/modules/keyboardmanager/ui/SingleKeyRemapControl.cpp index 226e766631..76959293d8 100644 --- a/src/modules/keyboardmanager/ui/SingleKeyRemapControl.cpp +++ b/src/modules/keyboardmanager/ui/SingleKeyRemapControl.cpp @@ -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(); - // 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.