03e5e77171
* Get the new settings values * Suppress windows menu appearing * Send new settings telemetry * Address PR comments
446 lines
12 KiB
C++
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;
|
|
}
|