diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/BlackmagicDesign.DaVinciResolve.en-US.yml b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/BlackmagicDesign.DaVinciResolve.en-US.yml new file mode 100644 index 000000000000..a54a7ab7a78f Binary files /dev/null and b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/BlackmagicDesign.DaVinciResolve.en-US.yml differ diff --git a/src/modules/dock/Dock/Dock.base.rc b/src/modules/dock/Dock/Dock.base.rc new file mode 100644 index 000000000000..3e246da9371b --- /dev/null +++ b/src/modules/dock/Dock/Dock.base.rc @@ -0,0 +1,42 @@ +#include +#include "resource.h" +#include "../../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#include "winres.h" +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO + FILEVERSION FILE_VERSION + PRODUCTVERSION PRODUCT_VERSION + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + diff --git a/src/modules/dock/Dock/Dock.vcxproj b/src/modules/dock/Dock/Dock.vcxproj new file mode 100644 index 000000000000..4783215d8c68 --- /dev/null +++ b/src/modules/dock/Dock/Dock.vcxproj @@ -0,0 +1,166 @@ + + + + + + + + + + Level3 + false + true + stdcpplatest + _UNICODE;UNICODE;%(PreprocessorDefinitions) + + + Windows + + + true + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + true + MultiThreadedDebug + + + true + + + + + NDEBUG;%(PreprocessorDefinitions) + MaxSpeed + false + MultiThreaded + true + true + + + true + true + true + + + + 16.0 + Win32Proj + {E1F6C9B2-3A5F-5B7C-0D9E-2F3A4B5C6D7E} + Dock + + + Application + Unicode + Spectre + + + true + true + + + false + true + false + + + + + + + + + + + + PowerToys.$(MSBuildProjectName) + $(RepoRoot)$(Platform)\$(Configuration)\ + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(RepoRoot)src\common\Telemetry;$(RepoRoot)src\common;$(RepoRoot)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + winmm.lib;shcore.lib;shlwapi.lib;DbgHelp.lib;uxtheme.lib;dwmapi.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(RepoRoot)src\common\Telemetry;$(RepoRoot)src\common;$(RepoRoot)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + winmm.lib;shcore.lib;shlwapi.lib;DbgHelp.lib;uxtheme.lib;dwmapi.lib;%(AdditionalDependencies) + + + + + + + Create + + + + + + + + + + + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {1d5be09d-78c0-4fd7-af00-ae7c1af7c525} + + + {8f021b46-362b-485c-bfba-ccf83e820cbd} + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + diff --git a/src/modules/dock/Dock/DockWindow.cpp b/src/modules/dock/Dock/DockWindow.cpp new file mode 100644 index 000000000000..282491defea4 --- /dev/null +++ b/src/modules/dock/Dock/DockWindow.cpp @@ -0,0 +1,566 @@ +#include "pch.h" +#include "DockWindow.h" + +#include +#include + +#include "ModuleConstants.h" + +DockWindow::DockWindow() +{ + m_defaultIcon = LoadIcon(nullptr, IDI_APPLICATION); + m_hStopEvent = CreateEventW(nullptr, TRUE, FALSE, NonLocalizable::StopEventName); + m_startTime = GetTickCount64(); +} + +DockWindow::~DockWindow() +{ + if (m_defaultIcon) + { + DestroyIcon(m_defaultIcon); + } + if (m_hStopEvent) + { + CloseHandle(m_hStopEvent); + } + ClearApps(); +} + +bool DockWindow::Create(HINSTANCE hInst) +{ + m_hInst = hInst; + + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(WNDCLASSEXW); + wc.lpfnWndProc = StaticWndProc; + wc.hInstance = hInst; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 35)); + wc.lpszClassName = L"PowerToysDockWindow"; + + if (!RegisterClassExW(&wc)) + { + Logger::error(L"DockWindow: Failed to register window class"); + return false; + } + + m_hwnd = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE, + wc.lpszClassName, + L"PowerToys Dock", + WS_POPUP, + 0, 0, 100, WINDOW_HEIGHT, + nullptr, nullptr, hInst, this); + + if (!m_hwnd) + { + Logger::error(L"DockWindow: Failed to create window"); + return false; + } + + return true; +} + +int DockWindow::Run() +{ + EnumerateWindows(); + PositionDock(); + ShowWindow(m_hwnd, SW_SHOWNOACTIVATE); + RegisterShellHook(); + + SetTimer(m_hwnd, AUTO_HIDE_TIMER_ID, AUTO_HIDE_MS, nullptr); + SetTimer(m_hwnd, REFRESH_TIMER_ID, 3000, nullptr); + + MSG msg = {}; + while (!m_exitRequested && GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + + if (m_hStopEvent && WaitForSingleObject(m_hStopEvent, 0) == WAIT_OBJECT_0) + { + m_exitRequested = true; + } + } + + KillTimer(m_hwnd, AUTO_HIDE_TIMER_ID); + KillTimer(m_hwnd, REFRESH_TIMER_ID); + UnregisterShellHook(); + + return 0; +} + +void DockWindow::Stop() +{ + m_exitRequested = true; + if (m_hStopEvent) + { + SetEvent(m_hStopEvent); + } + if (m_hwnd) + { + PostMessage(m_hwnd, WM_CLOSE, 0, 0); + } +} + +LRESULT CALLBACK DockWindow::StaticWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + DockWindow* self = nullptr; + if (msg == WM_NCCREATE) + { + auto cs = reinterpret_cast(lParam); + self = static_cast(cs->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(self)); + } + else + { + self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + } + + if (self) + { + return self->WndProc(msg, wParam, lParam); + } + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +LRESULT DockWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) + { + case WM_CREATE: + m_shellHookMsg = RegisterWindowMessageW(L"SHELLHOOK"); + return 0; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(m_hwnd, &ps); + + RECT clientRect; + GetClientRect(m_hwnd, &clientRect); + + HDC hdcMem = CreateCompatibleDC(hdc); + HBITMAP hBmp = CreateCompatibleBitmap(hdc, m_dockWidth, m_dockHeight); + HBITMAP hOldBmp = static_cast(SelectObject(hdcMem, hBmp)); + + HBRUSH hBgBrush = CreateSolidBrush(RGB(30, 30, 35)); + FillRect(hdcMem, &clientRect, hBgBrush); + DeleteObject(hBgBrush); + + for (size_t i = 0; i < m_apps.size(); ++i) + { + auto& app = m_apps[i]; + + if (app.active) + { + RECT activeBg = app.rect; + InflateRect(&activeBg, 2, 2); + HBRUSH hActiveBrush = CreateSolidBrush(RGB(55, 55, 60)); + FillRect(hdcMem, &activeBg, hActiveBrush); + DeleteObject(hActiveBrush); + } + + if (app.icon) + { + int iconX = app.rect.left + (app.rect.right - app.rect.left - ICON_SIZE) / 2; + int iconY = app.rect.top + 2; + DrawIconEx(hdcMem, iconX, iconY, app.icon, ICON_SIZE, ICON_SIZE, 0, nullptr, DI_NORMAL); + } + + SetBkMode(hdcMem, TRANSPARENT); + SetTextColor(hdcMem, RGB(220, 220, 220)); + + HFONT hFont = CreateFontW(10, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + HFONT hOldFont = static_cast(SelectObject(hdcMem, hFont)); + + RECT textRect = app.rect; + textRect.top = textRect.bottom - 16; + DrawTextW(hdcMem, app.title.c_str(), static_cast(app.title.length()), + &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + + SelectObject(hdcMem, hOldFont); + DeleteObject(hFont); + } + + BitBlt(hdc, 0, 0, m_dockWidth, m_dockHeight, hdcMem, 0, 0, SRCCOPY); + + SelectObject(hdcMem, hOldBmp); + DeleteObject(hBmp); + DeleteDC(hdcMem); + + EndPaint(m_hwnd, &ps); + return 0; + } + + case WM_TIMER: + { + UINT id = static_cast(wParam); + if (id == AUTO_HIDE_TIMER_ID) + { + CheckAutoHide(); + } + else if (id == REFRESH_TIMER_ID) + { + bool changed = false; + size_t oldCount = m_apps.size(); + EnumerateWindows(); + if (m_apps.size() != oldCount) + { + changed = true; + } + if (changed) + { + InvalidateRect(m_hwnd, nullptr, TRUE); + } + } + return 0; + } + + case WM_LBUTTONDOWN: + { + POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + int index = HitTest(pt); + if (index >= 0 && index < static_cast(m_apps.size())) + { + auto hwnd = m_apps[index].hwnd; + if (IsWindow(hwnd)) + { + if (IsIconic(hwnd)) + { + ShowWindow(hwnd, SW_RESTORE); + } + SetForegroundWindow(hwnd); + } + } + return 0; + } + + case WM_DISPLAYCHANGE: + PositionDock(); + InvalidateRect(m_hwnd, nullptr, TRUE); + return 0; + + default: + if (msg == m_shellHookMsg) + { + OnShellHook(wParam, lParam); + return 0; + } + break; + } + + return DefWindowProcW(m_hwnd, msg, wParam, lParam); +} + +void DockWindow::OnShellHook(WPARAM wParam, LPARAM lParam) +{ + auto hwnd = reinterpret_cast(lParam); + bool changed = false; + + switch (wParam) + { + case HSHELL_WINDOWCREATED: + case HSHELL_RUDEAPPACTIVATED: + if (IsAppWindow(hwnd)) + { + AddApp(hwnd); + changed = true; + } + break; + + case HSHELL_WINDOWDESTROYED: + RemoveApp(hwnd); + changed = true; + break; + + case HSHELL_WINDOWACTIVATED: + RefreshAppActive(hwnd); + changed = true; + break; + + case HSHELL_WINDOWREPLACED: + RemoveApp(hwnd); + changed = true; + break; + + case HSHELL_WINDOWREPLACING: + if (IsAppWindow(hwnd)) + { + AddApp(hwnd); + changed = true; + } + break; + } + + if (changed) + { + InvalidateRect(m_hwnd, nullptr, TRUE); + } +} + +void DockWindow::RegisterShellHook() +{ + if (m_hwnd && m_shellHookMsg) + { + RegisterShellHookWindow(m_hwnd); + } +} + +void DockWindow::UnregisterShellHook() +{ + if (m_hwnd) + { + DeregisterShellHookWindow(m_hwnd); + } +} + +void DockWindow::EnumerateWindows() +{ + struct EnumData + { + DockWindow* self; + }; + + EnumData data = { this }; + + EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL { + auto data = reinterpret_cast(lParam); + if (data->self->IsAppWindow(hwnd)) + { + data->self->AddApp(hwnd); + } + return TRUE; + }, reinterpret_cast(&data)); +} + +void DockWindow::AddApp(HWND hwnd) +{ + for (auto& app : m_apps) + { + if (app.hwnd == hwnd) + { + if (!app.icon) + { + app.icon = ExtractAppIcon(hwnd); + } + return; + } + } + + if (m_apps.size() >= 40) + { + return; + } + + AppItem app; + app.hwnd = hwnd; + app.icon = ExtractAppIcon(hwnd); + + wchar_t title[256] = {}; + GetWindowTextW(hwnd, title, 256); + app.title = title; + + if (app.title.empty()) + { + if (app.icon && app.icon != m_defaultIcon) + { + DestroyIcon(app.icon); + } + return; + } + + app.active = (hwnd == GetForegroundWindow()); + + m_apps.push_back(std::move(app)); + RebuildLayout(); +} + +void DockWindow::RemoveApp(HWND hwnd) +{ + for (auto it = m_apps.begin(); it != m_apps.end(); ++it) + { + if (it->hwnd == hwnd) + { + if (it->icon && it->icon != m_defaultIcon) + { + DestroyIcon(it->icon); + } + m_apps.erase(it); + RebuildLayout(); + return; + } + } +} + +void DockWindow::RefreshAppActive(HWND hwnd) +{ + for (auto& app : m_apps) + { + app.active = (app.hwnd == hwnd); + } +} + +void DockWindow::ClearApps() +{ + for (auto& app : m_apps) + { + if (app.icon && app.icon != m_defaultIcon) + { + DestroyIcon(app.icon); + } + } + m_apps.clear(); +} + +void DockWindow::RebuildLayout() +{ + int count = static_cast(m_apps.size()); + int totalWidth = count * (ICON_SIZE + ICON_PADDING) + ICON_PADDING; + m_dockWidth = (std::max)(totalWidth, 100); + + for (int i = 0; i < count; ++i) + { + int x = ICON_PADDING + i * (ICON_SIZE + ICON_PADDING); + int y = 2; + SetRect(&m_apps[i].rect, x, y, x + ICON_SIZE + ICON_PADDING / 2, y + ICON_SIZE + 18); + } + + PositionDock(); +} + +void DockWindow::PositionDock() +{ + int screenWidth = GetSystemMetrics(SM_CXSCREEN); + int screenHeight = GetSystemMetrics(SM_CYSCREEN); + + int x = (screenWidth - m_dockWidth) / 2; + int y = screenHeight - WINDOW_HEIGHT - 4; + + SetWindowPos(m_hwnd, HWND_TOPMOST, x, y, m_dockWidth, WINDOW_HEIGHT, SWP_NOACTIVATE); +} + +void DockWindow::CheckAutoHide() +{ + if (GetTickCount64() - m_startTime < 3000) + { + return; + } + + POINT pt; + GetCursorPos(&pt); + + int screenHeight = GetSystemMetrics(SM_CYSCREEN); + + RECT dockRect; + GetWindowRect(m_hwnd, &dockRect); + + bool mouseNearBottom = (pt.y >= screenHeight - SHOW_THRESHOLD); + bool mouseInDock = PtInRect(&dockRect, pt); + + if (mouseInDock || mouseNearBottom) + { + if (!m_visible) + { + m_visible = true; + SetWindowPos(m_hwnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + } + } + else if (m_visible) + { + m_visible = false; + ShowWindow(m_hwnd, SW_HIDE); + } +} + +bool DockWindow::IsAppWindow(HWND hwnd) const +{ + if (!IsWindowVisible(hwnd)) + { + return false; + } + + auto style = GetWindowLongPtrW(hwnd, GWL_STYLE); + auto exStyle = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + + if (!(style & WS_POPUP) && !(style & WS_OVERLAPPED)) + { + return false; + } + + if (exStyle & WS_EX_TOOLWINDOW) + { + return false; + } + + wchar_t className[64] = {}; + GetClassNameW(hwnd, className, 64); + if (wcscmp(className, L"Windows.UI.Core.CoreWindow") == 0 || + wcscmp(className, L"ApplicationFrameWindow") == 0 || + wcscmp(className, L"Progman") == 0 || + wcscmp(className, L"WorkerW") == 0) + { + return false; + } + + wchar_t title[256] = {}; + if (GetWindowTextW(hwnd, title, 256) == 0 || title[0] == L'\0') + { + return false; + } + + return true; +} + +HICON DockWindow::ExtractAppIcon(HWND hwnd) const +{ + HICON icon = reinterpret_cast(SendMessageW(hwnd, WM_GETICON, ICON_BIG, 0)); + if (!icon) + icon = reinterpret_cast(SendMessageW(hwnd, WM_GETICON, ICON_SMALL, 0)); + if (!icon) + icon = reinterpret_cast(GetClassLongPtrW(hwnd, GCLP_HICON)); + if (!icon) + icon = reinterpret_cast(GetClassLongPtrW(hwnd, GCLP_HICONSM)); + + if (!icon) + { + DWORD pid = 0; + GetWindowThreadProcessId(hwnd, &pid); + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (hProcess) + { + wchar_t path[MAX_PATH] = {}; + DWORD size = MAX_PATH; + if (QueryFullProcessImageNameW(hProcess, 0, path, &size)) + { + SHFILEINFOW sfi = {}; + if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_LARGEICON) && sfi.hIcon) + { + icon = sfi.hIcon; + } + } + CloseHandle(hProcess); + } + } + + return icon ? icon : m_defaultIcon; +} + +int DockWindow::HitTest(POINT pt) const +{ + for (size_t i = 0; i < m_apps.size(); ++i) + { + if (PtInRect(&m_apps[i].rect, pt)) + { + return static_cast(i); + } + } + return -1; +} diff --git a/src/modules/dock/Dock/DockWindow.h b/src/modules/dock/Dock/DockWindow.h new file mode 100644 index 000000000000..d9bd72b67456 --- /dev/null +++ b/src/modules/dock/Dock/DockWindow.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +struct AppItem +{ + HWND hwnd = nullptr; + HICON icon = nullptr; + std::wstring title; + RECT rect{}; + bool active = false; +}; + +class DockWindow +{ +public: + DockWindow(); + ~DockWindow(); + + bool Create(HINSTANCE hInst); + int Run(); + void Stop(); + +private: + static constexpr int ICON_SIZE = 48; + static constexpr int ICON_PADDING = 8; + static constexpr int WINDOW_HEIGHT = 72; + static constexpr UINT AUTO_HIDE_TIMER_ID = 100; + static constexpr UINT REFRESH_TIMER_ID = 101; + static constexpr int AUTO_HIDE_MS = 300; + static constexpr int SHOW_THRESHOLD = 4; + + static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + LRESULT WndProc(UINT msg, WPARAM wParam, LPARAM lParam); + + void OnShellHook(WPARAM wParam, LPARAM lParam); + void RegisterShellHook(); + void UnregisterShellHook(); + void EnumerateWindows(); + void AddApp(HWND hwnd); + void RemoveApp(HWND hwnd); + void RefreshAppActive(HWND hwnd); + void ClearApps(); + void RebuildLayout(); + + void PositionDock(); + void CheckAutoHide(); + bool IsAppWindow(HWND hwnd) const; + HICON ExtractAppIcon(HWND hwnd) const; + int HitTest(POINT pt) const; + + HWND m_hwnd = nullptr; + HINSTANCE m_hInst = nullptr; + HICON m_defaultIcon = nullptr; + HANDLE m_hStopEvent = nullptr; + std::vector m_apps; + UINT m_shellHookMsg = 0; + bool m_visible = true; + bool m_exitRequested = false; + int m_dockWidth = 100; + DWORD m_startTime = 0; +}; diff --git a/src/modules/dock/Dock/ModuleConstants.h b/src/modules/dock/Dock/ModuleConstants.h new file mode 100644 index 000000000000..64dfb21ee683 --- /dev/null +++ b/src/modules/dock/Dock/ModuleConstants.h @@ -0,0 +1,8 @@ +#pragma once + +namespace NonLocalizable +{ + const inline wchar_t ModuleKey[] = L"Dock"; + const inline wchar_t ModulePath[] = L"PowerToys.Dock.exe"; + const inline wchar_t StopEventName[] = L"Local\\PowerToys_Dock_StopEvent"; +} diff --git a/src/modules/dock/Dock/main.cpp b/src/modules/dock/Dock/main.cpp new file mode 100644 index 000000000000..08a4d2214364 --- /dev/null +++ b/src/modules/dock/Dock/main.cpp @@ -0,0 +1,80 @@ +#include "pch.h" + +#include +#include +#include +#include +#include + +#include + +#include "DockWindow.h" +#include "trace.h" + +const std::wstring moduleName = L"Dock"; +const std::wstring internalPath = L""; +const std::wstring instanceMutexName = L"Local\\PowerToys_Dock_InstanceMutex"; + +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ PWSTR lpCmdLine, _In_ int nCmdShow) +{ + Shared::Trace::ETWTrace trace; + trace.UpdateState(true); + + winrt::init_apartment(); + LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::dockLoggerName); + + if (powertoys_gpo::getConfiguredDockEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) + { + Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled."); + return 0; + } + + InitUnhandledExceptionHandler(); + + auto mutex = CreateMutex(nullptr, true, instanceMutexName.c_str()); + if (mutex == nullptr) + { + Logger::error(L"Failed to create mutex. {}", get_last_error_or_default(GetLastError())); + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) + { + return 0; + } + + auto mainThreadId = GetCurrentThreadId(); + + std::wstring pid = std::wstring(lpCmdLine); + if (!pid.empty()) + { + ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) { + if (err != ERROR_SUCCESS) + { + Logger::error(L"Failed to wait for parent process exit. {}", get_last_error_or_default(err)); + } + else + { + Logger::trace(L"PowerToys runner exited."); + } + + Logger::trace(L"Exiting Dock"); + PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); + }); + } + + Trace::Dock::RegisterProvider(); + + DockWindow dock; + if (!dock.Create(hInstance)) + { + Logger::error(L"DockWindow: Failed to create dock window"); + return 1; + } + + int result = dock.Run(); + + Trace::Dock::UnregisterProvider(); + trace.Flush(); + + return result; +} diff --git a/src/modules/dock/Dock/packages.config b/src/modules/dock/Dock/packages.config new file mode 100644 index 000000000000..6e6df9922b5e --- /dev/null +++ b/src/modules/dock/Dock/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/modules/dock/Dock/pch.cpp b/src/modules/dock/Dock/pch.cpp new file mode 100644 index 000000000000..1d9f38c57d63 --- /dev/null +++ b/src/modules/dock/Dock/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/dock/Dock/pch.h b/src/modules/dock/Dock/pch.h new file mode 100644 index 000000000000..ee61c4be6d44 --- /dev/null +++ b/src/modules/dock/Dock/pch.h @@ -0,0 +1,12 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#include +#include +#include +#include diff --git a/src/modules/dock/Dock/resource.base.h b/src/modules/dock/Dock/resource.base.h new file mode 100644 index 000000000000..2ac492c896d0 --- /dev/null +++ b/src/modules/dock/Dock/resource.base.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Dock.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys.Dock" +#define INTERNAL_NAME "PowerToys.Dock" +#define ORIGINAL_FILENAME "PowerToys.Dock.exe" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/dock/Dock/resource.h b/src/modules/dock/Dock/resource.h new file mode 100644 index 000000000000..2ac492c896d0 --- /dev/null +++ b/src/modules/dock/Dock/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Dock.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys.Dock" +#define INTERNAL_NAME "PowerToys.Dock" +#define ORIGINAL_FILENAME "PowerToys.Dock.exe" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/dock/Dock/trace.cpp b/src/modules/dock/Dock/trace.cpp new file mode 100644 index 000000000000..d9ae1f4b673a --- /dev/null +++ b/src/modules/dock/Dock/trace.cpp @@ -0,0 +1,26 @@ +#include "pch.h" +#include "trace.h" + +#include + +#define LoggingProviderKey "Microsoft.PowerToys" + +#define EventEnableDockKey "Dock_EnableDock" +#define EventEnabledKey "Enabled" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + LoggingProviderKey, + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::Dock::Enable(bool enabled) noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + EventEnableDockKey, + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, EventEnabledKey)); +} diff --git a/src/modules/dock/Dock/trace.h b/src/modules/dock/Dock/trace.h new file mode 100644 index 000000000000..5e2272f00141 --- /dev/null +++ b/src/modules/dock/Dock/trace.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class Trace +{ +public: + class Dock : public telemetry::TraceBase + { + public: + static void Enable(bool enabled) noexcept; + }; +}; diff --git a/src/modules/dock/DockModuleInterface/DockModuleInterface.rc b/src/modules/dock/DockModuleInterface/DockModuleInterface.rc new file mode 100644 index 000000000000..2f745311d9af --- /dev/null +++ b/src/modules/dock/DockModuleInterface/DockModuleInterface.rc @@ -0,0 +1,40 @@ +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#include "winres.h" +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/src/modules/dock/DockModuleInterface/DockModuleInterface.vcxproj b/src/modules/dock/DockModuleInterface/DockModuleInterface.vcxproj new file mode 100644 index 000000000000..c1d52c652955 --- /dev/null +++ b/src/modules/dock/DockModuleInterface/DockModuleInterface.vcxproj @@ -0,0 +1,81 @@ + + + + + + 15.0 + {D0C5B8A1-2E4F-4A6B-9C8E-1F2D3A4B5C6D} + Win32Proj + dock + DockModuleInterface + + + DynamicLibrary + + + + + + + + + + + $(RepoRoot)$(Platform)\$(Configuration)\ + + + PowerToys.DockModuleInterface + + + + _WINDOWS;_USRDLL;%(PreprocessorDefinitions) + ..\;$(RepoRoot)src\common\inc;$(RepoRoot)src\common\Telemetry;..\..\;$(RepoRoot)src\;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies) + + + + + + + + + + + + + Create + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + diff --git a/src/modules/dock/DockModuleInterface/dllmain.cpp b/src/modules/dock/DockModuleInterface/dllmain.cpp new file mode 100644 index 000000000000..5746c86da8e8 --- /dev/null +++ b/src/modules/dock/DockModuleInterface/dllmain.cpp @@ -0,0 +1,149 @@ +#include "pch.h" + +#include + +#include +#include +#include + +#include +#include + +#include +#include + +BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::Dock::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::Dock::UnregisterProvider(); + break; + } + return TRUE; +} + +class DockModuleInterface : public PowertoyModuleIface +{ +public: + virtual PCWSTR get_name() override + { + return L"Dock"; + } + + virtual const wchar_t* get_key() override + { + return NonLocalizable::ModuleKey; + } + + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + PowerToysSettings::Settings settings(hinstance, get_name()); + return settings.serialize_to_buffer(buffer, buffer_size); + } + + virtual void set_config(const wchar_t* config) override + { + try + { + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + values.save_to_settings_file(); + } + catch (std::exception&) + { + } + } + + virtual void enable() + { + Logger::info("Dock enabling"); + m_enabled = true; + + Trace::Dock::Enable(true); + + unsigned long powertoys_pid = GetCurrentProcessId(); + std::wstring executable_args = std::to_wstring(powertoys_pid); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI; + sei.lpFile = NonLocalizable::ModulePath; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (!ShellExecuteExW(&sei)) + { + Logger::error(L"Failed to start Dock"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + else + { + m_hProcess = sei.hProcess; + } + } + + virtual void disable() + { + Logger::info("Dock disabling"); + m_enabled = false; + + Trace::Dock::Enable(false); + + SetEvent(m_hStopEvent); + + if (WaitForSingleObject(m_hProcess, 1500) == WAIT_TIMEOUT) + { + if (m_hProcess) + { + TerminateProcess(m_hProcess, 0); + } + } + + if (m_hProcess) + { + CloseHandle(m_hProcess); + m_hProcess = nullptr; + } + } + + virtual bool is_enabled() override + { + return m_enabled; + } + + virtual void destroy() override + { + disable(); + if (m_hStopEvent) + { + CloseHandle(m_hStopEvent); + m_hStopEvent = nullptr; + } + delete this; + } + + DockModuleInterface() + { + m_hStopEvent = CreateEventW(nullptr, TRUE, FALSE, NonLocalizable::StopEventName); + } + +private: + bool m_enabled = false; + HANDLE m_hProcess = nullptr; + HANDLE m_hStopEvent = nullptr; +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new DockModuleInterface(); +} diff --git a/src/modules/dock/DockModuleInterface/packages.config b/src/modules/dock/DockModuleInterface/packages.config new file mode 100644 index 000000000000..6e6df9922b5e --- /dev/null +++ b/src/modules/dock/DockModuleInterface/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/modules/dock/DockModuleInterface/pch.cpp b/src/modules/dock/DockModuleInterface/pch.cpp new file mode 100644 index 000000000000..1d9f38c57d63 --- /dev/null +++ b/src/modules/dock/DockModuleInterface/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/dock/DockModuleInterface/pch.h b/src/modules/dock/DockModuleInterface/pch.h new file mode 100644 index 000000000000..5427d3452242 --- /dev/null +++ b/src/modules/dock/DockModuleInterface/pch.h @@ -0,0 +1,9 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include diff --git a/src/modules/dock/DockModuleInterface/resource.h b/src/modules/dock/DockModuleInterface/resource.h new file mode 100644 index 000000000000..b450fafd5079 --- /dev/null +++ b/src/modules/dock/DockModuleInterface/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by DockModuleInterface.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys Dock Module" +#define INTERNAL_NAME "PowerToys.DockModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.DockModuleInterface.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj index 6199ae3138c3..926a9c3a0858 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj @@ -8,8 +8,6 @@ enable disable True - true - true diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs index 4f500c833761..bfbb386054c8 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -11,6 +11,9 @@ using PowerAccent.Core.Tools; using PowerToys.PowerAccentKeyboardService; +using Windows.Win32; +using Windows.Win32.UI.Input.KeyboardAndMouse; + namespace PowerAccent.Core; public partial class PowerAccent : IDisposable @@ -41,10 +44,12 @@ public partial class PowerAccent : IDisposable private readonly KeyboardListener _keyboardListener; private readonly CharactersUsageInfo _usageInfo; + private readonly Action _uiThreadDispatcher; - public PowerAccent() + public PowerAccent(Action uiThreadDispatcher = null) { Logger.InitializeLogger("\\QuickAccent\\Logs"); + _uiThreadDispatcher = uiThreadDispatcher ?? (action => action()); LoadUnicodeInfoCache(); @@ -65,26 +70,17 @@ private void SetEvents() { _keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey) => { - System.Windows.Application.Current.Dispatcher.Invoke(() => - { - ShowToolbar(letterKey); - }); + _uiThreadDispatcher(() => ShowToolbar(letterKey)); })); _keyboardListener.SetHideToolbarEvent(new PowerToys.PowerAccentKeyboardService.HideToolbar((InputType inputType) => { - System.Windows.Application.Current.Dispatcher.Invoke(() => - { - SendInputAndHideToolbar(inputType); - }); + _uiThreadDispatcher(() => SendInputAndHideToolbar(inputType)); })); _keyboardListener.SetNextCharEvent(new PowerToys.PowerAccentKeyboardService.NextChar((TriggerKey triggerKey, bool shiftPressed) => { - System.Windows.Application.Current.Dispatcher.Invoke(() => - { - ProcessNextChar(triggerKey, shiftPressed); - }); + _uiThreadDispatcher(() => ProcessNextChar(triggerKey, shiftPressed)); })); _keyboardListener.SetIsLanguageLetterDelegate(new PowerToys.PowerAccentKeyboardService.IsLanguageLetter((LetterKey letterKey, out bool result) => @@ -208,13 +204,13 @@ private void SendInputAndHideToolbar(InputType inputType) case InputType.Right: { - SendKeys.SendWait("{RIGHT}"); + WindowsFunctions.SendVirtualKey(VIRTUAL_KEY.VK_RIGHT); break; } case InputType.Left: { - SendKeys.SendWait("{LEFT}"); + WindowsFunctions.SendVirtualKey(VIRTUAL_KEY.VK_LEFT); break; } diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs index c4f479c4d2b3..93f6d93f17ad 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs @@ -121,4 +121,39 @@ public static bool IsShiftState() var shift = PInvoke.GetAsyncKeyState((int)VIRTUAL_KEY.VK_SHIFT); return shift < 0; } + + public static void SendVirtualKey(VIRTUAL_KEY key) + { + unsafe + { + var inputs = new INPUT[] + { + new INPUT + { + type = INPUT_TYPE.INPUT_KEYBOARD, + Anonymous = new INPUT._Anonymous_e__Union + { + ki = new KEYBDINPUT + { + wVk = key, + }, + }, + }, + new INPUT + { + type = INPUT_TYPE.INPUT_KEYBOARD, + Anonymous = new INPUT._Anonymous_e__Union + { + ki = new KEYBDINPUT + { + wVk = key, + dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, + }, + }, + }, + }; + + _ = PInvoke.SendInput(inputs, Marshal.SizeOf()); + } + } } diff --git a/src/modules/poweraccent/PowerAccent.UI/App.xaml b/src/modules/poweraccent/PowerAccent.UI/App.xaml index e67322f3718a..776c7937c1de 100644 --- a/src/modules/poweraccent/PowerAccent.UI/App.xaml +++ b/src/modules/poweraccent/PowerAccent.UI/App.xaml @@ -1,6 +1,12 @@  \ No newline at end of file + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs index 297b5f2f7b88..02cce29e6cc0 100644 --- a/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs +++ b/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs @@ -2,63 +2,16 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Threading; -using System.Windows; +using Microsoft.UI.Xaml; -using ManagedCommon; -using Microsoft.PowerToys.Telemetry; +namespace PowerAccent.UI; -namespace PowerAccent.UI +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application, IDisposable - { - private static Mutex _mutex; - private bool _disposed; - private ETWTrace _etwTrace = new ETWTrace(); - - protected override void OnStartup(StartupEventArgs e) - { - _mutex = new Mutex(true, "QuickAccent", out bool createdNew); - - if (!createdNew) - { - Logger.LogWarning("Another running QuickAccent instance was detected. Exiting QuickAccent"); - Application.Current.Shutdown(); - } - - base.OnStartup(e); - } - - protected override void OnExit(ExitEventArgs e) - { - _mutex?.ReleaseMutex(); - base.OnExit(e); - } + private MainWindow _window; - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _mutex?.Dispose(); - _etwTrace?.Dispose(); - } - - _disposed = true; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + _window = new MainWindow(); } } diff --git a/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs b/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs deleted file mode 100644 index bcac370d7dfb..000000000000 --- a/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is locate (used if a resource is not found in the page, app, or any theme specific resource dictionaries) -] diff --git a/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml b/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml new file mode 100644 index 000000000000..f37f9ab8fa4a --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml.cs new file mode 100644 index 000000000000..28ee978c716f --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/MainWindow.xaml.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; +using WinUIEx; +using Point = PowerAccent.Core.Point; +using Size = PowerAccent.Core.Size; + +namespace PowerAccent.UI; + +public sealed partial class MainWindow : WindowEx, IDisposable, INotifyPropertyChanged +{ + private const SET_WINDOW_POS_FLAGS WindowPosFlags = + SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE; + + private readonly Core.PowerAccent _powerAccent; + private Visibility _characterNameVisibility = Visibility.Visible; + private int _selectedIndex = -1; + + public event PropertyChangedEventHandler PropertyChanged; + + public Visibility CharacterNameVisibility + { + get => _characterNameVisibility; + set + { + _characterNameVisibility = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CharacterNameVisibility))); + } + } + + public MainWindow() + { + InitializeComponent(); + + var dispatchQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + _powerAccent = new Core.PowerAccent(action => dispatchQueue.TryEnqueue(() => action())); + _powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay; + _powerAccent.OnSelectCharacter += PowerAccent_OnSelectionCharacter; + + var hwnd = this.GetWindowHandle(); + if (hwnd != IntPtr.Zero) + { + var exStyle = (WINDOW_EX_STYLE)PInvoke.GetWindowLong((HWND)hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + exStyle |= WINDOW_EX_STYLE.WS_EX_NOACTIVATE; + exStyle |= WINDOW_EX_STYLE.WS_EX_TOOLWINDOW; + exStyle |= WINDOW_EX_STYLE.WS_EX_TRANSPARENT; + PInvoke.SetWindowLong((HWND)hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)exStyle); + } + + AppWindow.IsShownInSwitchers = false; + AppWindow.Hide(); + + Closed += (s, e) => + { + _powerAccent.SaveUsageInfo(); + _powerAccent.Dispose(); + }; + } + + private void PowerAccent_OnSelectionCharacter(int index, string character) + { + _selectedIndex = index; + CharactersList.SelectedIndex = _selectedIndex; + + if (_selectedIndex >= 0 && _selectedIndex < _powerAccent.CharacterDescriptions.Length) + { + CharacterNameText.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; + } + + if (CharactersList.Items.Count > _selectedIndex && _selectedIndex >= 0) + { + CharactersList.ScrollIntoView(CharactersList.Items[_selectedIndex]); + } + } + + private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars) + { + AppWindow.IsTopMost = isActive; + + CharacterNameVisibility = _powerAccent.ShowUnicodeDescription ? Visibility.Visible : Visibility.Collapsed; + + if (isActive) + { + int offscreenX = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN) - 1000; + int offscreenY = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN) - 1000; + + var hwnd = this.GetWindowHandle(); + if (hwnd != IntPtr.Zero) + { + PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, offscreenX, offscreenY, 0, 0, WindowPosFlags); + } + + AppWindow.Show(); + SetWindowSize(); + CharactersList.ItemsSource = chars; + CharactersList.SelectedIndex = -1; + + CharactersList.UpdateLayout(); + + CharactersList.SelectedIndex = _selectedIndex; + + if (_selectedIndex >= 0 && _selectedIndex < chars.Length) + { + CharacterNameText.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; + CharactersList.ScrollIntoView(CharactersList.Items[_selectedIndex]); + CharactersList.UpdateLayout(); + } + else + { + CharacterNameText.Text = string.Empty; + } + + SetWindowPosition(); + Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Core.Telemetry.PowerAccentShowAccentMenuEvent()); + } + else + { + AppWindow.Hide(); + CharactersList.ItemsSource = null; + _selectedIndex = -1; + } + } + + private void SetWindowPosition() + { + double actualWidth = ContentBorder.ActualWidth; + double actualHeight = ContentBorder.ActualHeight; + Size windowSize = new Size(actualWidth, actualHeight); + Point physicalPosition = _powerAccent.GetDisplayCoordinates(windowSize); + + var hwnd = this.GetWindowHandle(); + if (hwnd != IntPtr.Zero) + { + PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, (int)Math.Round(physicalPosition.X), (int)Math.Round(physicalPosition.Y), 0, 0, WindowPosFlags); + } + } + + private void SetWindowSize() + { + double maxWidth = _powerAccent.GetDisplayMaxWidth(); + CharactersList.MaxWidth = maxWidth; + this.MaxWidth = maxWidth; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj index f800bd129011..a2eb867c1474 100644 --- a/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj +++ b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj @@ -1,40 +1,59 @@ - - - - WinExe - disable - true - True - icon.ico - app.manifest - PowerToys.PowerAccent - True - PowerAccent.UI.Program - $(RepoRoot)$(Platform)\$(Configuration) - false - false - - - - PreserveNewest - - + + PowerToys.PowerAccent + PowerToys Quick Accent + WinExe + $(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps + PowerAccent.UI + PowerToys.PowerAccent + app.manifest + true + false + false + true + None + true + icon.ico + True + DISABLE_XAML_GENERATED_MAIN,TRACE + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + - - - - - - + + + PreserveNewest + + + + + PowerToys.GPOWrapper;PowerToys.PowerAccentKeyboardService + $(OutDir) + false + + + + + + + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/Program.cs b/src/modules/poweraccent/PowerAccent.UI/Program.cs index e9a416f7e6a0..781bca8f208d 100644 --- a/src/modules/poweraccent/PowerAccent.UI/Program.cs +++ b/src/modules/poweraccent/PowerAccent.UI/Program.cs @@ -6,9 +6,10 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using System.Windows; using ManagedCommon; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; using PowerToys.Interop; namespace PowerAccent.UI; @@ -16,7 +17,6 @@ namespace PowerAccent.UI; internal static class Program { private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); - private static App _application; private static int _powerToysRunnerPid; [STAThread] @@ -34,9 +34,12 @@ public static void Main(string[] args) InitEvents(); - _application = new App(); - _application.InitializeComponent(); - _application.Run(); + Application.Start((p) => + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _ = new App(); + }); } private static void InitEvents() @@ -84,10 +87,11 @@ private static void Arguments(string[] args) private static void Terminate() { - Application.Current.Dispatcher.BeginInvoke(() => + if (DispatcherQueue.GetForCurrentThread() is DispatcherQueue queue) { - _tokenSource.Cancel(); - Application.Current.Shutdown(); - }); + queue.TryEnqueue(() => Application.Current.Exit()); + } + + _tokenSource.Cancel(); } } diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml deleted file mode 100644 index 0059e1efe546..000000000000 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs deleted file mode 100644 index b928d9bb00a4..000000000000 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.ComponentModel; -using System.Windows; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; -using Point = PowerAccent.Core.Point; -using Size = PowerAccent.Core.Size; - -namespace PowerAccent.UI; - -public partial class Selector : Window, IDisposable, INotifyPropertyChanged -{ - // When setting the position for the selector window, we do not alter the z-order, - // activation status, or size. - private const SET_WINDOW_POS_FLAGS WindowPosFlags = - SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE; - - private readonly Core.PowerAccent _powerAccent = new(); - - private Visibility _characterNameVisibility = Visibility.Visible; - - private int _selectedIndex = -1; - - public event PropertyChangedEventHandler PropertyChanged; - - public Visibility CharacterNameVisibility - { - get - { - return _characterNameVisibility; - } - - set - { - _characterNameVisibility = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CharacterNameVisibility))); - } - } - - public Selector() - { - InitializeComponent(); - - Application.Current.MainWindow.ShowActivated = false; - } - - protected override void OnSourceInitialized(EventArgs e) - { - base.OnSourceInitialized(e); - _powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay; - _powerAccent.OnSelectCharacter += PowerAccent_OnSelectionCharacter; - this.Visibility = Visibility.Hidden; - } - - private void PowerAccent_OnSelectionCharacter(int index, string character) - { - _selectedIndex = index; - characters.SelectedIndex = _selectedIndex; - - if (_selectedIndex >= 0 && _selectedIndex < _powerAccent.CharacterDescriptions.Length) - { - characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; - } - - if (characters.Items.Count > _selectedIndex && _selectedIndex >= 0) - { - characters.ScrollIntoView(characters.Items[_selectedIndex]); - } - } - - private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars) - { - // Topmost is conditionally set here to address hybrid graphics issues on laptops. - this.Topmost = isActive; - - CharacterNameVisibility = _powerAccent.ShowUnicodeDescription ? Visibility.Visible : Visibility.Collapsed; - - if (isActive) - { - int offscreenX = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_XVIRTUALSCREEN) - 1000; - int offscreenY = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_YVIRTUALSCREEN) - 1000; - - var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle; - if (hwnd != IntPtr.Zero) - { - // Move off-screen to avoid flicker on previous monitor before Show() and - // UpdateLayout(). - PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, offscreenX, offscreenY, 0, 0, WindowPosFlags); - } - else - { - this.Left = offscreenX; - this.Top = offscreenY; - } - - Show(); - SetWindowsSize(); - characters.ItemsSource = chars; - characters.SelectedIndex = -1; // Reset before setting dynamically to avoid flashing - - this.UpdateLayout(); // Required for filling the actual width/height before positioning. - - characters.SelectedIndex = _selectedIndex; - - if (_selectedIndex >= 0 && _selectedIndex < chars.Length) - { - characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; - characters.ScrollIntoView(characters.Items[_selectedIndex]); - this.UpdateLayout(); // Re-layout after scrolling - } - else - { - characterName.Text = string.Empty; - } - - SetWindowPosition(); - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent()); - } - else - { - Hide(); - characters.ItemsSource = null; - _selectedIndex = -1; - } - } - - private void MenuExit_Click(object sender, RoutedEventArgs e) - { - Application.Current.Shutdown(); - } - - private void SetWindowPosition() - { - Size windowSize = new(((FrameworkElement)Application.Current.MainWindow.Content).ActualWidth, ((FrameworkElement)Application.Current.MainWindow.Content).ActualHeight); - Point physicalPosition = _powerAccent.GetDisplayCoordinates(windowSize); - - var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle; - if (hwnd != IntPtr.Zero) - { - PInvoke.SetWindowPos((HWND)hwnd, (HWND)IntPtr.Zero, (int)Math.Round(physicalPosition.X), (int)Math.Round(physicalPosition.Y), 0, 0, WindowPosFlags); - } - } - - protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) - { - base.OnDpiChanged(oldDpi, newDpi); - if (this.Visibility == Visibility.Visible) - { - SetWindowsSize(); - SetWindowPosition(); - } - } - - private void SetWindowsSize() - { - double maxWidth = _powerAccent.GetDisplayMaxWidth(); - this.characters.MaxWidth = maxWidth; - this.MaxWidth = maxWidth; - } - - private void Window_SizeChanged(object sender, SizeChangedEventArgs e) - { - if (this.Visibility == Visibility.Visible) - { - SetWindowPosition(); - } - } - - protected override void OnClosed(EventArgs e) - { - _powerAccent.SaveUsageInfo(); - _powerAccent.Dispose(); - base.OnClosed(e); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - } -} diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml index 1d095efa7600..0c20c9d1fbc3 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml @@ -235,7 +235,7 @@ Grid.Column="1" VerticalAlignment="Center" helpers:SliderExtensions.IsMouseWheelEnabled="True" - helpers:SliderExtensions.MouseWheelChange="5" + helpers:SliderExtensions.MouseWheelChange="{x:Bind ViewModel.MouseWheelChange, Mode=OneWay}" IsEnabled="{x:Bind ViewModel.IsLinkedBrightnessSliderEnabled, Mode=OneWay}" IsTabStop="True" Maximum="100" @@ -525,13 +525,13 @@ Grid.Column="1" VerticalAlignment="Center" helpers:SliderExtensions.IsMouseWheelEnabled="True" - helpers:SliderExtensions.MouseWheelChange="5" + helpers:SliderExtensions.MouseWheelChange="{x:Bind MouseWheelChange, Mode=OneWay}" IsEnabled="{x:Bind IsBrightnessSliderEnabled, Mode=OneWay}" IsTabStop="True" Maximum="100" Minimum="0" Value="{x:Bind Brightness, Mode=TwoWay}" /> - + /// Gets or sets a value indicating whether brightness slider changes are broadcast to all /// non-excluded monitors as one linked level. Persisted in PowerDisplaySettings so @@ -487,6 +490,9 @@ private void LoadUIDisplaySettings() LinkedLevelsActive = settings.Properties.LinkedLevelsActive; + // Load mouse wheel increment for slider controls + MouseWheelChange = settings.Properties.MouseWheelChange; + // Load custom VCP mappings (now using shared type from PowerDisplay.Common.Models) CustomVcpMappings = settings.Properties.CustomVcpMappings?.ToList() ?? new List(); Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings"); diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs index f16c2349d8e5..efbdc947e358 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs @@ -210,6 +210,9 @@ private async Task ApplyPropertyToHardwareAsync( // Property to access IsInteractionEnabled from parent ViewModel public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true; + // Property to access MouseWheelChange from parent ViewModel + public double MouseWheelChange => _mainViewModel?.MouseWheelChange ?? 5; + public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel) { _monitor = monitor; @@ -866,6 +869,10 @@ private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEvent OnPropertyChanged(nameof(ShowBrightnessSlider)); OnPropertyChanged(nameof(ShowExcludeButton)); } + else if (e.PropertyName == nameof(MainViewModel.MouseWheelChange)) + { + OnPropertyChanged(nameof(MouseWheelChange)); + } } private void OnMonitorPropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs index 1b087d0fcbcf..98485c9d4677 100644 --- a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs @@ -25,6 +25,7 @@ public PowerDisplayProperties() ShowIdentifyMonitorsButton = true; MaxCompatibilityMode = false; LinkedLevelsActive = false; + MouseWheelChange = 5; ExcludedFromSyncMonitorIds = new List(); CustomVcpMappings = new List(); @@ -107,5 +108,12 @@ public HotkeySettings ActivationShortcut /// [JsonPropertyName("custom_vcp_mappings")] public List CustomVcpMappings { get; set; } + + /// + /// Gets or sets the mouse wheel increment for slider controls (brightness, contrast, volume). + /// Default is 5. Higher values make the slider change more per wheel notch. + /// + [JsonPropertyName("mouse_wheel_change")] + public int MouseWheelChange { get; set; } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml index ecb3f58e0ecd..825ec68b395c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml @@ -96,6 +96,15 @@ + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 76042ede6d7a..d11e4717e0c5 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -5793,6 +5793,12 @@ The break timer font matches the text font. Show identify monitors button + + Mouse wheel increment + + + Amount the brightness, contrast, and volume sliders change per mouse wheel notch (1-25). + Custom VCP name mappings diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs index c958b5d93ee4..d9c58c93489e 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs @@ -343,6 +343,19 @@ public bool ShowIdentifyMonitorsButton } } + public int MouseWheelChange + { + get => _settings.Properties.MouseWheelChange; + set + { + if (SetSettingsProperty(_settings.Properties.MouseWheelChange, value, v => _settings.Properties.MouseWheelChange = v)) + { + SignalSettingsUpdated(); + Logger.LogInfo($"MouseWheelChange changed to {value}"); + } + } + } + public HotkeySettings ActivationShortcut { get => _settings.Properties.ActivationShortcut;