Files
PowerToys/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
Den Delimarsky 27dcd1e5bc Awake and DevEx improvements (#44795)
This PR contains a set of bug fixes and general improvements to
[Awake](https://awake.den.dev/) and developer experience tooling for
building the module.

### Awake Fixes

- **#32544** - Fixed an issue where Awake settings became non-functional
after the PC wakes from sleep. Added `WM_POWERBROADCAST` handling to
detect system resume events (`PBT_APMRESUMEAUTOMATIC`,
`PBT_APMRESUMESUSPEND`) and re-apply `SetThreadExecutionState` to
restore the awake state.

- **#36150** - Fixed an issue where Awake would not prevent sleep when
AC power is connected. Added `PBT_APMPOWERSTATUSCHANGE` handling to
re-apply `SetThreadExecutionState` when the power source changes
(AC/battery transitions).

- **#41674** - Fixed silent failure when `SetThreadExecutionState`
fails. The monitor thread now handles the return value, logs an error,
and reverts to passive mode with updated tray icon.

- **#41738** - Fixed `--display-on` CLI flag default from `true` to
`false` to align with documentation and PowerToys settings behavior.
This is a breaking change for scripts relying on the undocumented
default.

- **#41918** - Fixed `WM_COMMAND` message processing flaw in
`TrayHelper.WndProc` that incorrectly compared enum values against enum
count. Added proper bounds checking for custom tray time entries.

- **#44134** - Documented that `ES_DISPLAY_REQUIRED` (used when "Keep
display on" is enabled) blocks Task Scheduler idle detection, preventing
scheduled maintenance tasks like SSD TRIM. Workaround: disable "Keep
display on" or manually run `Optimize-Volume -DriveLetter C -ReTrim`.

- **#38770** - Fixed tray icon failing to appear after Windows updates.
Increased retry attempts and delays for icon Add operations (10
attempts, up to ~15.5 seconds total) while keeping existing fast retry
behavior for Update/Delete operations.

- **#40501** - Fixed tray icon not disappearing when Awake is disabled.
The `SetShellIcon` function was incorrectly requiring an icon for Delete
operations, causing the `NIM_DELETE` message to never be sent.

- Fixed an issue where toggling "Keep screen on" during an active timed
session would disrupt the countdown timer. The display setting now
updates directly without restarting the timer, preserving the exact
remaining time.

### Performance Optimizations

- Fixed O(n²) loop in `TrayHelper.CreateAwakeTimeSubMenu` by replacing
`ElementAt(i)` with `foreach` iteration.

- Fixed Observable subscription leak in `Manager.cs` by storing
`IDisposable` and disposing in `CancelExistingThread()`. Also removed
dead `_tokenSource` code that was no longer used.

- Reduced allocations in `SingleThreadSynchronizationContext` by
changing `Tuple<>` to `ValueTuple`.

- Replaced dedicated exit event thread with
`ThreadPool.RegisterWaitForSingleObject()` to reduce resource usage.

### Code Quality

- Replaced `Console.WriteLine` with `Logger.LogError` in `TrayHelper.cs`
for consistent logging.

- Added proper error logging to silent exception catches in
`AwakeService.cs`.

- Removed dead `Math.Min(minutes, int.MaxValue)` code where `minutes` is
already an `int`.

- Extracted hardcoded tray icon ID to named constant `TrayIconId`.

- Standardized null coalescing for `GetSettings<AwakeSettings>()` calls
across all files.

### Debugging Experience Fixes

- Fixed first-chance exceptions in `settings_window.cpp` during
debugging. Added `HasKey()` check before accessing `hotkey_changed`
property to prevent `hresult_error` exceptions when the property doesn't
exist in module settings.

- Fixed first-chance exceptions in FindMyMouse `parse_settings` during
debugging. Refactored to extract the properties object once and added
`HasKey()` checks before all `GetNamedObject()` calls. This prevents
`winrt::hresult_error` exceptions when optional settings keys (like
legacy `overlay_opacity`) don't exist, improving the debugging
experience by eliminating spurious exception breaks.

- Fixed LightSwitch.UITests build failures when building from a clean
state. Added missing project references (`ManagedCommon`,
`LightSwitchModuleInterface`) with `ReferenceOutputAssembly=false` to
ensure proper build ordering, and added existence check for the native
DLL copy operation.

### Developer Experience

- Added `setup-dev-environment.ps1` script to automate development
environment setup.
- Added `clean-artifacts.ps1` script to resolve build errors from
corrupted build state or missing image files.
- Added build script that allows standalone command line build of the
Awake module.
- Added troubleshooting section to
`doc/devdocs/development/debugging.md` with guidance on resolving common
build errors.
2026-01-20 21:27:45 -08:00

622 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include "FindMyMouse.h"
#include "WinHookEventIDs.h"
#include <thread>
#include <common/utils/logger_helper.h>
#include <common/utils/color.h>
#include <common/utils/string_utils.h>
#include <common/utils/EventWaiter.h>
#include <common/interop/shared_constants.h>
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_METHOD[] = L"activation_method";
const wchar_t JSON_KEY_INCLUDE_WIN_KEY[] = L"include_win_key";
const wchar_t JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode";
const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color";
const wchar_t JSON_KEY_SPOTLIGHT_COLOR[] = L"spotlight_color";
const wchar_t JSON_KEY_OVERLAY_OPACITY[] = L"overlay_opacity"; // legacy only (migrated into color alpha)
const wchar_t JSON_KEY_SPOTLIGHT_RADIUS[] = L"spotlight_radius";
const wchar_t JSON_KEY_ANIMATION_DURATION_MS[] = L"animation_duration_ms";
const wchar_t JSON_KEY_SPOTLIGHT_INITIAL_ZOOM[] = L"spotlight_initial_zoom";
const wchar_t JSON_KEY_EXCLUDED_APPS[] = L"excluded_apps";
const wchar_t JSON_KEY_SHAKING_MINIMUM_DISTANCE[] = L"shaking_minimum_distance";
const wchar_t JSON_KEY_SHAKING_INTERVAL_MS[] = L"shaking_interval_ms";
const wchar_t JSON_KEY_SHAKING_FACTOR[] = L"shaking_factor";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
HMODULE m_hModule;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
m_hModule = hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"FindMyMouse";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"Focus the mouse pointer";
// Implement the PowerToy Module Interface and all the required methods.
class FindMyMouse : public PowertoyModuleIface
{
private:
// The PowerToy state.
bool m_enabled = false;
// Hotkey to invoke the module
HotkeyEx m_hotkey;
// Find My Mouse specific settings
FindMyMouseSettings m_findMyMouseSettings;
// Event-driven trigger support
EventWaiter m_triggerEventWaiter;
// Load initial settings from the persisted values.
void init_settings();
// Helper function to extract the settings
void parse_settings(PowerToysSettings::PowerToyValues& settings);
public:
// Constructor
FindMyMouse()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::findMyMouseLoggerName);
init_settings();
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
// Ensure threads/handles are cleaned up before destruction
disable();
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return MODULE_NAME;
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredFindMyMouseEnabledValue();
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t* action) override
{
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_settings(values);
FindMyMouseApplySettings(m_findMyMouseSettings);
}
catch (std::exception&)
{
// Improper JSON.
}
}
// Enable the powertoy
virtual void enable()
{
m_enabled = true;
Trace::EnableFindMyMouse(true);
std::thread([=]() { FindMyMouseMain(m_hModule, m_findMyMouseSettings); }).detach();
// Start listening for external trigger event so we can invoke the same logic as the hotkey.
m_triggerEventWaiter.start(CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});
}
// Disable the powertoy
virtual void disable()
{
m_enabled = false;
Trace::EnableFindMyMouse(false);
FindMyMouseDisable();
m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
virtual std::optional<HotkeyEx> GetHotkeyEx() override
{
Logger::trace("GetHotkeyEx()");
if (m_findMyMouseSettings.activationMethod == FindMyMouseActivationMethod::Shortcut)
{
return m_hotkey;
}
return std::nullopt;
}
virtual void OnHotkeyEx() override
{
Logger::trace("OnHotkeyEx()");
HWND hwnd = GetSonarHwnd();
if (hwnd != nullptr)
{
PostMessageW(hwnd, WM_PRIV_SHORTCUT, NULL, NULL);
}
}
};
// Load the settings file.
void FindMyMouse::init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(FindMyMouse::get_key());
parse_settings(settings);
}
catch (std::exception&)
{
// Error while loading from the settings file. Let default values stay as they are.
}
}
inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent)
{
if (overlayOpacityPercent < 0)
{
return 255; // fallback: fully opaque
}
if (overlayOpacityPercent > 100)
{
overlayOpacityPercent = 100;
}
// Round to nearest integer (0255)
return static_cast<uint8_t>((overlayOpacityPercent * 255 + 50) / 100);
}
void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
FindMyMouseSettings findMyMouseSettings;
if (!settingsObject.GetView().Size())
{
Logger::info("Find My Mouse settings are empty");
m_findMyMouseSettings = findMyMouseSettings;
return;
}
// Early exit if no properties object exists
if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
Logger::info("Find My Mouse settings have no properties");
m_findMyMouseSettings = findMyMouseSettings;
return;
}
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
// Parse Activation Method
if (properties.HasKey(JSON_KEY_ACTIVATION_METHOD))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value < static_cast<int>(FindMyMouseActivationMethod::EnumElements) && value >= 0)
{
std::wstring version = (std::wstring)settingsObject.GetNamedString(L"version");
if (version == L"1.0" && value == 1)
{
findMyMouseSettings.activationMethod = FindMyMouseActivationMethod::ShakeMouse;
}
else
{
findMyMouseSettings.activationMethod = static_cast<FindMyMouseActivationMethod>(value);
}
}
else
{
throw std::runtime_error("Invalid Activation Method value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
}
}
// Parse Include Win Key
if (properties.HasKey(JSON_KEY_INCLUDE_WIN_KEY))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to get 'include windows key with ctrl' setting");
}
}
// Parse Do Not Activate On Game Mode
if (properties.HasKey(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
findMyMouseSettings.doNotActivateOnGameMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to get 'do not activate on game mode' setting");
}
}
// Colors + legacy overlay opacity migration
// Desired behavior:
// - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
// - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
int legacyOverlayOpacity = -1;
bool backgroundColorHadExplicitAlpha = false;
bool spotlightColorHadExplicitAlpha = false;
// Parse Legacy Overlay Opacity (may not exist in newer settings)
if (properties.HasKey(JSON_KEY_OVERLAY_OPACITY))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0 && value <= 100)
{
legacyOverlayOpacity = value;
}
}
catch (...)
{
// overlay_opacity may have invalid data
}
}
// Parse Background Color
if (properties.HasKey(JSON_KEY_BACKGROUND_COLOR))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
auto backgroundColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a = 255, r, g, b;
bool parsed = false;
if (checkValidARGB(backgroundColorStr, &a, &r, &g, &b))
{
parsed = true;
backgroundColorHadExplicitAlpha = true; // New schema with alpha present
}
else if (checkValidRGB(backgroundColorStr, &r, &g, &b))
{
a = LegacyOpacityToAlpha(legacyOverlayOpacity);
parsed = true; // Old schema (no alpha component)
}
if (parsed)
{
findMyMouseSettings.backgroundColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
}
else
{
Logger::error("Background color value is invalid. Will use default");
}
}
catch (...)
{
Logger::warn("Failed to initialize background color from settings. Will use default value");
}
}
// Parse Spotlight Color
if (properties.HasKey(JSON_KEY_SPOTLIGHT_COLOR))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
auto spotlightColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a = 255, r, g, b;
bool parsed = false;
if (checkValidARGB(spotlightColorStr, &a, &r, &g, &b))
{
parsed = true;
spotlightColorHadExplicitAlpha = true;
}
else if (checkValidRGB(spotlightColorStr, &r, &g, &b))
{
a = LegacyOpacityToAlpha(legacyOverlayOpacity);
parsed = true;
}
if (parsed)
{
findMyMouseSettings.spotlightColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
}
else
{
Logger::error("Spotlight color value is invalid. Will use default");
}
}
catch (...)
{
Logger::warn("Failed to initialize spotlight color from settings. Will use default value");
}
}
// Parse Spotlight Radius
if (properties.HasKey(JSON_KEY_SPOTLIGHT_RADIUS))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.spotlightRadius = value;
}
else
{
throw std::runtime_error("Invalid Spotlight Radius value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Spotlight Radius from settings. Will use default value");
}
}
// Parse Animation Duration
if (properties.HasKey(JSON_KEY_ANIMATION_DURATION_MS))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.animationDurationMs = value;
}
else
{
throw std::runtime_error("Invalid Animation Duration value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Animation Duration from settings. Will use default value");
}
}
// Parse Spotlight Initial Zoom
if (properties.HasKey(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.spotlightInitialZoom = value;
}
else
{
throw std::runtime_error("Invalid Spotlight Initial Zoom value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Spotlight Initial Zoom from settings. Will use default value");
}
}
// Parse Excluded Apps
if (properties.HasKey(JSON_KEY_EXCLUDED_APPS))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_EXCLUDED_APPS);
std::wstring apps = jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE).c_str();
std::vector<std::wstring> excludedApps;
auto excludedUppercase = apps;
CharUpperBuffW(excludedUppercase.data(), static_cast<DWORD>(excludedUppercase.length()));
std::wstring_view view(excludedUppercase);
view = left_trim<wchar_t>(trim<wchar_t>(view));
while (!view.empty())
{
auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length());
excludedApps.emplace_back(view.substr(0, pos));
view.remove_prefix(pos);
view = left_trim<wchar_t>(trim<wchar_t>(view));
}
findMyMouseSettings.excludedApps = excludedApps;
}
catch (...)
{
Logger::warn("Failed to initialize Excluded Apps from settings. Will use default value");
}
}
// Parse Shaking Minimum Distance
if (properties.HasKey(JSON_KEY_SHAKING_MINIMUM_DISTANCE))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.shakeMinimumDistance = value;
}
else
{
throw std::runtime_error("Invalid Shaking Minimum Distance value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Shaking Minimum Distance from settings. Will use default value");
}
}
// Parse Shaking Interval Milliseconds
if (properties.HasKey(JSON_KEY_SHAKING_INTERVAL_MS))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.shakeIntervalMs = value;
}
else
{
throw std::runtime_error("Invalid Shaking Interval Milliseconds value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Shaking Interval Milliseconds from settings. Will use default value");
}
}
// Parse Shaking Factor
if (properties.HasKey(JSON_KEY_SHAKING_FACTOR))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_FACTOR);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
findMyMouseSettings.shakeFactor = value;
}
else
{
throw std::runtime_error("Invalid Shaking Factor value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Shaking Factor from settings. Will use default value");
}
}
// Parse HotKey
if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
{
try
{
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_hotkey = HotkeyEx();
if (hotkey.win_pressed())
{
m_hotkey.modifiersMask |= MOD_WIN;
}
if (hotkey.ctrl_pressed())
{
m_hotkey.modifiersMask |= MOD_CONTROL;
}
if (hotkey.shift_pressed())
{
m_hotkey.modifiersMask |= MOD_SHIFT;
}
if (hotkey.alt_pressed())
{
m_hotkey.modifiersMask |= MOD_ALT;
}
m_hotkey.vkCode = static_cast<WORD>(hotkey.get_code());
}
catch (...)
{
Logger::warn("Failed to initialize Activation Shortcut from settings. Will use default value");
}
}
if (!m_hotkey.modifiersMask)
{
Logger::info("Using default Activation Shortcut");
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = 0x46; // F key
}
m_findMyMouseSettings = findMyMouseSettings;
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new FindMyMouse();
}