From 16528888df33b3703198339e8af78fc2b8a7bb69 Mon Sep 17 00:00:00 2001 From: Andrey Nekrasov Date: Wed, 1 Jul 2020 12:37:50 +0300 Subject: [PATCH] Shortcut guide: add support for hotkeys + comments (#4517) --- src/common/d2d_window.cpp | 17 +++- src/common/d2d_window.h | 8 +- src/common/start_visible.cpp | 18 ++-- src/modules/shortcut_guide/overlay_window.cpp | 14 ++- src/modules/shortcut_guide/overlay_window.h | 4 +- src/modules/shortcut_guide/shortcut_guide.cpp | 34 ++++++- src/modules/shortcut_guide/shortcut_guide.h | 3 + src/modules/shortcut_guide/target_state.cpp | 92 ++++++++++++++----- src/modules/shortcut_guide/target_state.h | 18 ++-- 9 files changed, 160 insertions(+), 48 deletions(-) diff --git a/src/common/d2d_window.cpp b/src/common/d2d_window.cpp index 2556b48aa4..15f1e72b3b 100644 --- a/src/common/d2d_window.cpp +++ b/src/common/d2d_window.cpp @@ -3,7 +3,8 @@ extern "C" IMAGE_DOS_HEADER __ImageBase; -D2DWindow::D2DWindow() +D2DWindow::D2DWindow(std::optional>> _pre_wnd_proc) : + pre_wnd_proc(std::move(_pre_wnd_proc)) { static const WCHAR* class_name = L"PToyD2DPopup"; WNDCLASS wc = {}; @@ -36,6 +37,7 @@ void D2DWindow::show(UINT x, UINT y, UINT width, UINT height) } base_resize(width, height); render_empty(); + hidden = false; on_show(); SetWindowPos(hwnd, HWND_TOPMOST, x, y, width, height, 0); ShowWindow(hwnd, SW_SHOWNORMAL); @@ -44,6 +46,7 @@ void D2DWindow::show(UINT x, UINT y, UINT width, UINT height) void D2DWindow::hide() { + hidden = true; ShowWindow(hwnd, SW_HIDE); on_hide(); } @@ -185,6 +188,11 @@ D2DWindow* D2DWindow::this_from_hwnd(HWND window) LRESULT __stdcall D2DWindow::d2d_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { + auto self = this_from_hwnd(window); + if (self && self->pre_wnd_proc.has_value()) + { + (*self->pre_wnd_proc)(window, message, wparam, lparam); + } switch (message) { case WM_NCCREATE: @@ -195,11 +203,12 @@ LRESULT __stdcall D2DWindow::d2d_window_proc(HWND window, UINT message, WPARAM w } case WM_MOVE: case WM_SIZE: - this_from_hwnd(window)->base_resize((unsigned)lparam & 0xFFFF, (unsigned)lparam >> 16); - // Fall through to call 'base_render()' + self->base_resize((unsigned)lparam & 0xFFFF, (unsigned)lparam >> 16); + [[fallthrough]]; case WM_PAINT: - this_from_hwnd(window)->base_render(); + self->base_render(); return 0; + default: return DefWindowProc(window, message, wparam, lparam); } diff --git a/src/common/d2d_window.h b/src/common/d2d_window.h index 7e7e2ffeac..94cc02b71b 100644 --- a/src/common/d2d_window.h +++ b/src/common/d2d_window.h @@ -11,10 +11,13 @@ #include #include "d2d_svg.h" +#include +#include + class D2DWindow { public: - D2DWindow(); + D2DWindow(std::optional>> pre_wnd_proc = std::nullopt); void show(UINT x, UINT y, UINT width, UINT height); void hide(); void initialize(); @@ -43,6 +46,7 @@ protected: void render_empty(); std::recursive_mutex mutex; + bool hidden = true; bool initialized = false; HWND hwnd; UINT window_width, window_height; @@ -58,4 +62,6 @@ protected: winrt::com_ptr d2d_factory; winrt::com_ptr d2d_device; winrt::com_ptr d2d_dc; + + std::optional>> pre_wnd_proc; }; diff --git a/src/common/start_visible.cpp b/src/common/start_visible.cpp index 421d7e4204..17efa76b7d 100644 --- a/src/common/start_visible.cpp +++ b/src/common/start_visible.cpp @@ -3,15 +3,21 @@ bool is_start_visible() { - static winrt::com_ptr app_visibility; + static const auto app_visibility = []() { + winrt::com_ptr result; + CoCreateInstance(CLSID_AppVisibility, + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(result), + result.put_void()); + return result; + }(); + if (!app_visibility) { - winrt::check_hresult(CoCreateInstance(CLSID_AppVisibility, - nullptr, - CLSCTX_INPROC_SERVER, - __uuidof(app_visibility), - app_visibility.put_void())); + return false; } + BOOL visible; auto result = app_visibility->IsLauncherVisible(&visible); return SUCCEEDED(result) && visible; diff --git a/src/modules/shortcut_guide/overlay_window.cpp b/src/modules/shortcut_guide/overlay_window.cpp index e482c10cf0..b778902832 100644 --- a/src/modules/shortcut_guide/overlay_window.cpp +++ b/src/modules/shortcut_guide/overlay_window.cpp @@ -184,8 +184,8 @@ D2D1_RECT_F D2DOverlaySVG::get_snap_right() const return result; } -D2DOverlayWindow::D2DOverlayWindow() : - total_screen({}), animation(0.3) +D2DOverlayWindow::D2DOverlayWindow(std::optional>> pre_wnd_proc) : + total_screen({}), animation(0.3), D2DWindow(std::move(pre_wnd_proc)) { tasklist_thread = std::thread([&] { while (running) @@ -213,6 +213,7 @@ D2DOverlayWindow::D2DOverlayWindow() : void D2DOverlayWindow::show(HWND active_window, bool snappable) { std::unique_lock lock(mutex); + hidden = false; tasklist_buttons.clear(); this->active_window = active_window; this->active_window_snappable = snappable; @@ -473,6 +474,11 @@ void D2DOverlayWindow::quick_hide() } } +HWND D2DOverlayWindow::get_window_handle() +{ + return hwnd; +} + float D2DOverlayWindow::get_overlay_opacity() { return overlay_opacity; @@ -619,12 +625,12 @@ void D2DOverlayWindow::hide_thumbnail() void D2DOverlayWindow::render(ID2D1DeviceContext5* d2d_dc) { - if (!winkey_held() || is_start_visible()) + if (!hidden && !instance->overlay_visible()) { hide(); - instance->was_hidden(); return; } + d2d_dc->Clear(); int x_offset = 0, y_offset = 0, dimension = 0; auto current_anim_value = (float)animation.value(Animation::AnimFunctions::LINEAR); diff --git a/src/modules/shortcut_guide/overlay_window.h b/src/modules/shortcut_guide/overlay_window.h index f6ac4e0a62..340b3cf3fa 100644 --- a/src/modules/shortcut_guide/overlay_window.h +++ b/src/modules/shortcut_guide/overlay_window.h @@ -46,7 +46,7 @@ struct AnimateKeys class D2DOverlayWindow : public D2DWindow { public: - D2DOverlayWindow(); + D2DOverlayWindow(std::optional>> pre_wnd_proc = std::nullopt); void show(HWND active_window, bool snappable); void animate(int vk_code); ~D2DOverlayWindow(); @@ -54,6 +54,8 @@ public: void set_theme(const std::wstring& theme); void quick_hide(); + HWND get_window_handle(); + private: void animate(int vk_code, int offset); bool show_thumbnail(const RECT& rect, double alpha); diff --git a/src/modules/shortcut_guide/shortcut_guide.cpp b/src/modules/shortcut_guide/shortcut_guide.cpp index fff6658c05..f7bf519b92 100644 --- a/src/modules/shortcut_guide/shortcut_guide.cpp +++ b/src/modules/shortcut_guide/shortcut_guide.cpp @@ -10,6 +10,7 @@ extern "C" IMAGE_DOS_HEADER __ImageBase; +// TODO: refactor singleton OverlayWindow* instance = nullptr; namespace @@ -125,12 +126,36 @@ void OverlayWindow::set_config(const wchar_t* config) } } +constexpr int alternative_switch_hotkey_id = 0x2; +constexpr UINT alternative_switch_modifier_mask = MOD_WIN | MOD_SHIFT; +constexpr UINT alternative_switch_vk_code = VK_OEM_2; + void OverlayWindow::enable() { + auto switcher = [&](HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + if (msg == WM_KEYDOWN && wparam == VK_ESCAPE && instance->target_state->active()) + { + instance->target_state->toggle_force_shown(); + return 0; + } + if (msg != WM_HOTKEY) + { + return 0; + } + const auto vk_code = HIWORD(lparam); + const auto modifiers_mask = LOWORD(lparam); + if (alternative_switch_vk_code != vk_code || alternative_switch_modifier_mask != modifiers_mask) + { + return 0; + } + instance->target_state->toggle_force_shown(); + return 0; + }; + if (!_enabled) { Trace::EnableShortcutGuide(true); - winkey_popup = std::make_unique(); + winkey_popup = std::make_unique(std::move(switcher)); winkey_popup->apply_overlay_opacity(((float)overlayOpacity.value) / 100.0f); winkey_popup->set_theme(theme.value); target_state = std::make_unique(pressTime.value); @@ -148,6 +173,7 @@ void OverlayWindow::enable() MessageBoxW(NULL, L"Cannot install keyboard listener.", L"PowerToys - Shortcut Guide", MB_OK | MB_ICONERROR); } } + RegisterHotKey(winkey_popup->get_window_handle(), alternative_switch_hotkey_id, alternative_switch_modifier_mask, alternative_switch_vk_code); } _enabled = true; } @@ -161,6 +187,7 @@ void OverlayWindow::disable(bool trace_event) { Trace::EnableShortcutGuide(false); } + UnregisterHotKey(winkey_popup->get_window_handle(), alternative_switch_hotkey_id); winkey_popup->hide(); target_state->exit(); target_state.reset(); @@ -241,6 +268,11 @@ void OverlayWindow::destroy() instance = nullptr; } +bool OverlayWindow::overlay_visible() const +{ + return target_state->active(); +} + void OverlayWindow::init_settings() { try diff --git a/src/modules/shortcut_guide/shortcut_guide.h b/src/modules/shortcut_guide/shortcut_guide.h index c9f69679dc..5d207ab13a 100644 --- a/src/modules/shortcut_guide/shortcut_guide.h +++ b/src/modules/shortcut_guide/shortcut_guide.h @@ -14,6 +14,7 @@ class OverlayWindow : public PowertoyModuleIface { public: OverlayWindow(); + virtual const wchar_t* get_name() override; virtual const wchar_t** get_events() override; virtual bool get_config(wchar_t* buffer, int* buffer_size) override; @@ -39,6 +40,8 @@ public: virtual void destroy() override; + bool overlay_visible() const; + private: std::wstring app_name; std::unique_ptr target_state; diff --git a/src/modules/shortcut_guide/target_state.cpp b/src/modules/shortcut_guide/target_state.cpp index d5f6e7761c..8cb81fe3eb 100644 --- a/src/modules/shortcut_guide/target_state.cpp +++ b/src/modules/shortcut_guide/target_state.cpp @@ -5,36 +5,39 @@ #include "common/shared_constants.h" TargetState::TargetState(int ms_delay) : - delay(std::chrono::milliseconds(ms_delay)), thread(&TargetState::thread_proc, this) + // TODO: All this processing should be done w/o a separate thread etc. in pre_wnd_proc of winkey_popup to avoid + // multithreading. Use SetTimer for delayed events + delay(std::chrono::milliseconds(ms_delay)), + thread(&TargetState::thread_proc, this) { } +constexpr unsigned VK_S = 0x53; + bool TargetState::signal_event(unsigned vk_code, bool key_down) { std::unique_lock lock(mutex); + // Ignore repeated key presses if (!events.empty() && events.back().key_down == key_down && events.back().vk_code == vk_code) { return false; } - // Hide the overlay when WinKey + Shift + S is pressed. 0x53 is the VK code of the S key - if (key_down && state == Shown && vk_code == 0x53 && (GetKeyState(VK_LSHIFT) || GetKeyState(VK_RSHIFT))) + // Hide the overlay when WinKey + Shift + S is pressed + if (key_down && state == Shown && vk_code == VK_S && (GetKeyState(VK_LSHIFT) || GetKeyState(VK_RSHIFT))) { // We cannot use normal hide() here, there is stuff that needs deinitialization. // It can be safely done when the user releases the WinKey. instance->quick_hide(); } - bool suppress = false; - if (!key_down && (vk_code == VK_LWIN || vk_code == VK_RWIN) && - state == Shown && - std::chrono::system_clock::now() - signal_timestamp > std::chrono::milliseconds(300) && - !key_was_pressed) - { - suppress = true; - } + const bool win_key_released = !key_down && (vk_code == VK_LWIN || vk_code == VK_RWIN); + constexpr auto overlay_fade_in_animation_time = std::chrono::milliseconds(300); + const auto overlay_active = state == Shown && (std::chrono::system_clock::now() - signal_timestamp > overlay_fade_in_animation_time); + const bool suppress_win_release = win_key_released && (state == ForceShown || overlay_active) && !nonwin_key_was_pressed_during_shown; + events.push_back({ key_down, vk_code }); lock.unlock(); cv.notify_one(); - if (suppress) + if (suppress_win_release) { // Send a fake key-stroke to prevent the start menu from appearing. // We use 0xCF VK code, which is reserved. It still prevents the @@ -54,12 +57,17 @@ bool TargetState::signal_event(unsigned vk_code, bool key_down) input[2].ki.dwExtraInfo = CommonSharedConstants::KEYBOARDMANAGER_INJECTED_FLAG; SendInput(3, input, sizeof(INPUT)); } - return suppress; + return suppress_win_release; } void TargetState::was_hidden() { - std::unique_lock lock(mutex); + std::unique_lock lock(mutex); + // Ignore callbacks from the D2DOverlayWindow + if (state == ForceShown) + { + return; + } state = Hidden; events.clear(); lock.unlock(); @@ -98,7 +106,7 @@ void TargetState::handle_hidden() } } -void TargetState::handle_shown() +void TargetState::handle_shown(const bool forced) { std::unique_lock lock(mutex); if (events.empty()) @@ -110,19 +118,18 @@ void TargetState::handle_shown() return; } auto event = next(); - if (event.key_down && (event.vk_code == VK_LWIN || event.vk_code == VK_RWIN)) + if (event.vk_code == VK_LWIN || event.vk_code == VK_RWIN) { + if (!forced && (!event.key_down || !winkey_held())) + { + state = Hidden; + } return; } - if (!event.key_down && (event.vk_code == VK_LWIN || event.vk_code == VK_RWIN) || !winkey_held()) - { - state = Hidden; - lock.unlock(); - return; - } + if (event.key_down) { - key_was_pressed = true; + nonwin_key_was_pressed_during_shown = true; lock.unlock(); instance->on_held_press(event.vk_code); } @@ -141,7 +148,10 @@ void TargetState::thread_proc() handle_timeout(); break; case Shown: - handle_shown(); + handle_shown(false); + break; + case ForceShown: + handle_shown(true); break; case Exiting: default: @@ -155,9 +165,15 @@ void TargetState::handle_timeout() std::unique_lock lock(mutex); auto wait_time = delay - (std::chrono::system_clock::now() - winkey_timestamp); if (events.empty()) + { cv.wait_for(lock, wait_time); + } if (state == Exiting) + { return; + } + + // Skip all VK_*WIN-down events while (!events.empty()) { auto event = events.front(); @@ -166,15 +182,20 @@ void TargetState::handle_timeout() else break; } + // If we've detected that a user is holding anything other than VK_*WIN or start menu is visible, we should hide if (!events.empty() || !only_winkey_key_held() || is_start_visible()) { state = Hidden; return; } + if (std::chrono::system_clock::now() - winkey_timestamp < delay) + { return; + } + signal_timestamp = std::chrono::system_clock::now(); - key_was_pressed = false; + nonwin_key_was_pressed_during_shown = false; state = Shown; lock.unlock(); instance->on_held(); @@ -182,5 +203,26 @@ void TargetState::handle_timeout() void TargetState::set_delay(int ms_delay) { + std::unique_lock lock(mutex); delay = std::chrono::milliseconds(ms_delay); } + +void TargetState::toggle_force_shown() +{ + std::unique_lock lock(mutex); + events.clear(); + if (state != ForceShown) + { + state = ForceShown; + instance->on_held(); + } + else + { + state = Hidden; + } +} + +bool TargetState::active() const +{ + return state == ForceShown || state == Shown; +} diff --git a/src/modules/shortcut_guide/target_state.h b/src/modules/shortcut_guide/target_state.h index 8d1a867a7d..cfcb46758b 100644 --- a/src/modules/shortcut_guide/target_state.h +++ b/src/modules/shortcut_guide/target_state.h @@ -21,24 +21,30 @@ public: void exit(); void set_delay(int ms_delay); + void toggle_force_shown(); + bool active() const; + private: KeyEvent next(); void handle_hidden(); void handle_timeout(); - void handle_shown(); + void handle_shown(const bool forced); void thread_proc(); - std::mutex mutex; - std::condition_variable cv; + std::recursive_mutex mutex; + std::condition_variable_any cv; std::chrono::system_clock::time_point winkey_timestamp, signal_timestamp; std::chrono::milliseconds delay; std::deque events; - enum + enum State { Hidden, Timeout, Shown, + ForceShown, Exiting - } state = Hidden; - bool key_was_pressed = false; + }; + std::atomic state = Hidden; + + bool nonwin_key_was_pressed_during_shown = false; std::thread thread; };