PowerToys/src/modules/ShortcutGuide/ShortcutGuide/shortcut_guide.cpp
Jaime Bernardo 03e5e77171 [Shortcut Guide] Suppress Windows Menu appearing (#13553)
* Get the new settings values

* Suppress windows menu appearing

* Send new settings telemetry

* Address PR comments
2021-10-11 11:39:30 +01:00

446 lines
12 KiB
C++

#include "pch.h"
#include "shortcut_guide.h"
#include "target_state.h"
#include "trace.h"
#include <common/SettingsAPI/settings_objects.h>
#include <common/debug_control.h>
#include <common/interop/shared_constants.h>
#include <sstream>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/logger/logger.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <common/utils/string_utils.h>
#include <common/utils/winapi_error.h>
#include <common/utils/window.h>
#include <Psapi.h>
#include <common/hooks/LowlevelKeyboardEvent.h>
// TODO: refactor singleton
OverlayWindow* instance = nullptr;
namespace
{
// Window properties relevant to ShortcutGuide
struct ShortcutGuideWindowInfo
{
HWND hwnd = nullptr; // Handle to the top-level foreground window or nullptr if there is no such window
bool snappable = false; // True, if the window can react to Windows Snap keys
bool disabled = false;
};
ShortcutGuideWindowInfo GetShortcutGuideWindowInfo(HWND active_window)
{
ShortcutGuideWindowInfo result;
active_window = GetAncestor(active_window, GA_ROOT);
if (!IsWindowVisible(active_window))
{
return result;
}
auto style = GetWindowLong(active_window, GWL_STYLE);
auto exStyle = GetWindowLong(active_window, GWL_EXSTYLE);
if ((style & WS_CHILD) == WS_CHILD ||
(style & WS_DISABLED) == WS_DISABLED ||
(exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW ||
(exStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE)
{
return result;
}
std::array<char, 256> class_name;
GetClassNameA(active_window, class_name.data(), static_cast<int>(class_name.size()));
if (is_system_window(active_window, class_name.data()))
{
return result;
}
static HWND cortana_hwnd = nullptr;
if (cortana_hwnd == nullptr)
{
if (strcmp(class_name.data(), "Windows.UI.Core.CoreWindow") == 0 &&
get_process_path(active_window).ends_with(L"SearchUI.exe"))
{
cortana_hwnd = active_window;
return result;
}
}
else if (cortana_hwnd == active_window)
{
return result;
}
result.hwnd = active_window;
// In reality, Windows Snap works if even one of those styles is set
// for a window, it is just limited. If there is no WS_MAXIMIZEBOX using
// WinKey + Up just won't maximize the window. Similary, without
// WS_MINIMIZEBOX the window will not get minimized. A "Save As..." dialog
// is a example of such window - it can be snapped to both sides and to
// all screen corners, but will not get maximized nor minimized.
// For now, since ShortcutGuide can only disable entire "Windows Controls"
// group, we require that the window supports all the options.
result.snappable = ((style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX) &&
((style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX) &&
((style & WS_THICKFRAME) == WS_THICKFRAME);
return result;
}
const LPARAM eventActivateWindow = 1;
bool wasWinPressed = false;
bool isWinPressed()
{
return (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
}
// all modifiers without win key
std::vector<int> modifierKeys = { VK_SHIFT, VK_LSHIFT, VK_RSHIFT, VK_CONTROL, VK_LCONTROL, VK_RCONTROL, VK_MENU, VK_LMENU, VK_RMENU };
// returns false if there are other modifiers pressed or win key isn' pressed
bool onlyWinPressed()
{
if (!isWinPressed())
{
return false;
}
for (auto key : modifierKeys)
{
if (GetAsyncKeyState(key) & 0x8000)
{
return false;
}
}
return true;
}
bool isWin(int key)
{
return key == VK_LWIN || key == VK_RWIN;
}
bool isKeyDown(LowlevelKeyboardEvent event)
{
return event.wParam == WM_KEYDOWN || event.wParam == WM_SYSKEYDOWN;
}
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
LowlevelKeyboardEvent event;
if (nCode == HC_ACTION)
{
event.lParam = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
event.wParam = wParam;
if (event.lParam->vkCode == VK_ESCAPE)
{
Logger::trace(L"ESC key was pressed");
instance->CloseWindow(HideWindowType::ESC_PRESSED);
}
if (wasWinPressed && !isKeyDown(event) && isWin(event.lParam->vkCode))
{
Logger::trace(L"Win key was released");
instance->CloseWindow(HideWindowType::WIN_RELEASED);
}
if (isKeyDown(event) && isWin(event.lParam->vkCode))
{
wasWinPressed = true;
}
if (onlyWinPressed() && isKeyDown(event) && !isWin(event.lParam->vkCode))
{
Logger::trace(L"Shortcut with win key was pressed");
instance->CloseWindow(HideWindowType::WIN_SHORTCUT_PRESSED);
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
std::wstring ToWstring(HideWindowType type)
{
switch (type)
{
case HideWindowType::ESC_PRESSED:
return L"ESC_PRESSED";
case HideWindowType::WIN_RELEASED:
return L"WIN_RELEASED";
case HideWindowType::WIN_SHORTCUT_PRESSED:
return L"WIN_SHORTCUT_PRESSED";
case HideWindowType::THE_SHORTCUT_PRESSED:
return L"THE_SHORTCUT_PRESSED";
}
return L"";
}
}
OverlayWindow::OverlayWindow(HWND activeWindow)
{
instance = this;
this -> activeWindow = activeWindow;
app_name = GET_RESOURCE_STRING(IDS_SHORTCUT_GUIDE);
Logger::info("Overlay Window is creating");
init_settings();
keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), NULL);
if (!keyboardHook)
{
Logger::warn(L"Failed to create low level keyboard hook. {}", get_last_error_or_default(GetLastError()));
}
}
void OverlayWindow::ShowWindow()
{
winkey_popup = std::make_unique<D2DOverlayWindow>();
winkey_popup->apply_overlay_opacity(((float)overlayOpacity.value) / 100.0f);
winkey_popup->set_theme(theme.value);
target_state = std::make_unique<TargetState>();
try
{
winkey_popup->initialize();
}
catch (...)
{
Logger::critical("Winkey popup failed to initialize");
return;
}
target_state->toggle_force_shown();
}
void OverlayWindow::CloseWindow(HideWindowType type, int mainThreadId)
{
if (mainThreadId == 0)
{
mainThreadId = GetCurrentThreadId();
}
if (this->winkey_popup)
{
if (shouldReactToPressedWinKey.value)
{
// Send a dummy key to prevent Start Menu from activating
INPUT dummyEvent[1] = {};
dummyEvent[0].type = INPUT_KEYBOARD;
dummyEvent[0].ki.wVk = 0xFF;
dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, dummyEvent, sizeof(INPUT));
}
this->winkey_popup->SetWindowCloseType(ToWstring(type));
Logger::trace(L"Terminating process");
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
}
}
bool OverlayWindow::IsDisabled()
{
WCHAR exePath[MAX_PATH] = L"";
instance->get_exe_path(activeWindow, exePath);
if (wcslen(exePath) > 0)
{
return is_disabled_app(exePath);
}
return false;
}
OverlayWindow::~OverlayWindow()
{
if (event_waiter)
{
event_waiter.reset();
}
if (winkey_popup)
{
winkey_popup->hide();
}
if (target_state)
{
target_state->exit();
target_state.reset();
}
if (winkey_popup)
{
winkey_popup.reset();
}
if (keyboardHook)
{
UnhookWindowsHookEx(keyboardHook);
}
}
void OverlayWindow::on_held()
{
auto windowInfo = GetShortcutGuideWindowInfo(activeWindow);
if (windowInfo.disabled)
{
target_state->was_hidden();
return;
}
winkey_popup->show(windowInfo.hwnd, windowInfo.snappable);
}
void OverlayWindow::quick_hide()
{
winkey_popup->quick_hide();
}
void OverlayWindow::was_hidden()
{
target_state->was_hidden();
}
bool OverlayWindow::overlay_visible() const
{
return target_state->active();
}
void OverlayWindow::init_settings()
{
auto settings = GetSettings();
overlayOpacity.value = settings.overlayOpacity;
theme.value = settings.theme;
disabledApps.value = settings.disabledApps;
shouldReactToPressedWinKey.value = settings.shouldReactToPressedWinKey;
update_disabled_apps();
}
bool OverlayWindow::is_disabled_app(wchar_t* exePath)
{
if (exePath == nullptr)
{
return false;
}
auto exePathUpper = std::wstring(exePath);
CharUpperBuffW(exePathUpper.data(), (DWORD)exePathUpper.length());
for (const auto& row : disabled_apps_array)
{
const auto pos = exePathUpper.rfind(row);
const auto last_slash = exePathUpper.rfind('\\');
// Check that row occurs in disabled_apps_array, and its last occurrence contains in itself the first character after the last backslash.
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
{
return true;
}
}
return false;
}
void OverlayWindow::update_disabled_apps()
{
disabled_apps_array.clear();
auto disabledUppercase = disabledApps.value;
CharUpperBuffW(disabledUppercase.data(), (DWORD)disabledUppercase.length());
std::wstring_view view(disabledUppercase);
view = trim(view);
while (!view.empty())
{
auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length());
disabled_apps_array.emplace_back(view.substr(0, pos));
view.remove_prefix(pos);
view = trim(view);
}
}
void OverlayWindow::get_exe_path(HWND window, wchar_t* path)
{
if (disabled_apps_array.empty())
{
return;
}
DWORD pid = 0;
GetWindowThreadProcessId(window, &pid);
if (pid != 0)
{
HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (processHandle && GetProcessImageFileName(processHandle, path, MAX_PATH) > 0)
{
CloseHandle(processHandle);
}
}
}
ShortcutGuideSettings OverlayWindow::GetSettings() noexcept
{
ShortcutGuideSettings settings;
json::JsonObject properties;
try
{
PowerToysSettings::PowerToyValues settingsValues =
PowerToysSettings::PowerToyValues::load_from_settings_file(app_key);
auto settingsObject = settingsValues.get_raw_json();
if (!settingsObject.GetView().Size())
{
return settings;
}
properties = settingsObject.GetNamedObject(L"properties");
}
catch (...)
{
Logger::warn("Failed to read settings. Use default settings");
return settings;
}
try
{
settings.hotkey = PowerToysSettings::HotkeyObject::from_json(properties.GetNamedObject(OpenShortcut::name)).to_string();
}
catch (...)
{
}
try
{
settings.overlayOpacity = (int)properties.GetNamedObject(OverlayOpacity::name).GetNamedNumber(L"value");
}
catch (...)
{
}
try
{
settings.shouldReactToPressedWinKey = properties.GetNamedObject(ShouldReactToPressedWinKey::name).GetNamedBoolean(L"value");
}
catch (...)
{
}
try
{
settings.windowsKeyPressTime = (int)properties.GetNamedObject(WindowsKeyPressTime::name).GetNamedNumber(L"value");
}
catch (...)
{
}
try
{
settings.theme = (std::wstring)properties.GetNamedObject(Theme::name).GetNamedString(L"value");
}
catch (...)
{
}
try
{
settings.disabledApps = (std::wstring)properties.GetNamedObject(DisabledApps::name).GetNamedString(L"value");
}
catch (...)
{
}
return settings;
}