Compare commits
24 Commits
autoUpgrad
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f69c07cfe9 | ||
|
|
e7a093391d | ||
|
|
11ed0b36ee | ||
|
|
2185180be8 | ||
|
|
968cfce9c4 | ||
|
|
5a426480a2 | ||
|
|
dc57d22754 | ||
|
|
3d715b66ce | ||
|
|
99ef5948b0 | ||
|
|
218a01c1a9 | ||
|
|
1c7f3d832c | ||
|
|
ff87dce4a4 | ||
|
|
758a60103c | ||
|
|
f8cadbf7f0 | ||
|
|
0819a6268b | ||
|
|
a31f82fcbd | ||
|
|
daeb2e1ef4 | ||
|
|
b31f94d6a4 | ||
|
|
fbebac92e6 | ||
|
|
7b1bbe851e | ||
|
|
6e5202e29c | ||
|
|
391cd62a0d | ||
|
|
621846bfc8 | ||
|
|
3a9578af56 |
2
.github/actions/spell-check/excludes.txt
vendored
@@ -107,7 +107,7 @@
|
||||
^src/common/sysinternals/Eula/
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
|
||||
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^doc/devdocs/modules/cmdpal/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
|
||||
2
.github/actions/spell-check/expect.txt
vendored
@@ -1641,6 +1641,7 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
traies
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
@@ -1661,6 +1662,7 @@ UBR
|
||||
UCallback
|
||||
ucrt
|
||||
ucrtd
|
||||
udit
|
||||
uefi
|
||||
uesc
|
||||
UFlags
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
<Folder Name="/common/interop/">
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 491 KiB After Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 706 KiB |
|
Before Width: | Height: | Size: 408 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 492 KiB After Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 311 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 890 KiB After Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 858 KiB After Width: | Height: | Size: 858 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@@ -14,8 +14,6 @@
|
||||
#include <common/updating/updating.h>
|
||||
#include <common/updating/updateState.h>
|
||||
#include <common/updating/installer.h>
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/HttpClient.h>
|
||||
@@ -23,8 +21,6 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
@@ -42,16 +38,15 @@ namespace fs = std::filesystem;
|
||||
|
||||
std::optional<fs::path> CopySelfToTempDir()
|
||||
{
|
||||
// D5 fix: Use unique temp path with PID to avoid collision on concurrent updates
|
||||
std::error_code error;
|
||||
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
|
||||
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
|
||||
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
|
||||
if (error)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return dst_path;
|
||||
return std::move(dst_path);
|
||||
}
|
||||
|
||||
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
@@ -62,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
|
||||
auto state = UpdateState::read();
|
||||
|
||||
// Handle readyToInstall first — the installer is already on disk,
|
||||
// so we don't need a GitHub API call (which may fail if offline).
|
||||
if (state.state == UpdateState::readyToInstall)
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
else if (state.state == UpdateState::readyToInstall)
|
||||
{
|
||||
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
|
||||
if (fs::is_regular_file(installer))
|
||||
@@ -77,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::upToDate)
|
||||
else if (state.state == UpdateState::upToDate)
|
||||
{
|
||||
isUpToDate = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
|
||||
// Check for error BEFORE dereferencing — the old code crashed here
|
||||
// when GitHub API was unreachable (new_version_info held an error string).
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
|
||||
Logger::error("Invoked with -update_now argument, but update state was invalid");
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -128,29 +116,13 @@ bool InstallNewVersionStage1(fs::path installer)
|
||||
|
||||
if (pt_main_window != nullptr)
|
||||
{
|
||||
// Get the process that owns the tray window so we can wait for it to exit
|
||||
DWORD ptProcessId = 0;
|
||||
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
|
||||
|
||||
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
|
||||
|
||||
// D4 fix: Wait for PT to actually exit before launching installer.
|
||||
// Without this, the installer may find PT files locked.
|
||||
if (ptProcessId != 0)
|
||||
{
|
||||
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
|
||||
if (ptProcess)
|
||||
{
|
||||
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the install directory so Stage 2 can relaunch PowerToys after install
|
||||
const std::wstring installDir = get_module_folderpath();
|
||||
|
||||
std::wstring arguments = updating::BuildStage2Arguments(
|
||||
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
|
||||
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
|
||||
arguments += L" \"";
|
||||
arguments += installer.c_str();
|
||||
arguments += L"\"";
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = copy_in_temp->c_str();
|
||||
@@ -218,16 +190,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
|
||||
if (!args || nArgs < 2)
|
||||
{
|
||||
if (args)
|
||||
{
|
||||
LocalFree(args);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// D3 fix: ensure args is freed on all exit paths
|
||||
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
|
||||
|
||||
std::wstring_view action{ args[1] };
|
||||
|
||||
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
|
||||
@@ -236,10 +201,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
|
||||
if (action == UPDATE_NOW_LAUNCH_STAGE1)
|
||||
{
|
||||
// Backup config files before the update to protect against corruption
|
||||
Logger::info("Backing up config files before update");
|
||||
updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
bool isUpToDate = false;
|
||||
auto installerPath = ObtainInstaller(isUpToDate);
|
||||
bool failed = !installerPath.has_value();
|
||||
@@ -256,12 +217,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
}
|
||||
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
|
||||
{
|
||||
if (nArgs < 3)
|
||||
{
|
||||
Logger::error("Stage 2 invoked without installer path argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
const bool failed = !InstallNewVersionStage2(args[2]);
|
||||
if (failed)
|
||||
@@ -272,37 +227,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
state.state = UpdateState::errorDownloading;
|
||||
});
|
||||
}
|
||||
|
||||
// D7 fix: Always check for corrupted configs after Stage 2, regardless
|
||||
// of install success/failure. A failed install may still corrupt configs.
|
||||
Logger::info("Checking for corrupted config files after update");
|
||||
updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
if (!failed)
|
||||
{
|
||||
// Relaunch PowerToys from the install directory
|
||||
if (updating::CanRelaunchAfterUpdate(nArgs))
|
||||
{
|
||||
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
|
||||
|
||||
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = ptExePath.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = UPDATE_REPORT_SUCCESS;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::error(L"Failed to relaunch PowerToys after update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace ExprtkCalculator::internal
|
||||
std::wstring ToWStringFullPrecision(double value)
|
||||
{
|
||||
std::wostringstream oss;
|
||||
oss.imbue(std::locale::classic());
|
||||
oss << std::fixed << std::setprecision(15) << value;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
@@ -1,679 +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.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace UpdatingUnitTests
|
||||
{
|
||||
// Helper to create a temp directory for test isolation.
|
||||
// Each instance gets a unique subdirectory to prevent test interference.
|
||||
class TempDir
|
||||
{
|
||||
public:
|
||||
TempDir()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH + 1];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
static std::atomic<int> counter{0};
|
||||
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
|
||||
|
||||
// Ensure clean state
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
fs::create_directories(m_path, ec);
|
||||
}
|
||||
|
||||
~TempDir()
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
}
|
||||
|
||||
const fs::path& path() const { return m_path; }
|
||||
|
||||
// Write a file with the given content
|
||||
void WriteFile(const fs::path& relativePath, const std::string& content)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(content.data(), content.size());
|
||||
}
|
||||
|
||||
// Write a file with raw bytes (including null bytes for corruption testing)
|
||||
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
// Read file content as string
|
||||
std::string ReadFile(const fs::path& relativePath)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
std::ifstream file(fullPath, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool FileExists(const fs::path& relativePath)
|
||||
{
|
||||
return fs::exists(m_path / relativePath);
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path;
|
||||
};
|
||||
|
||||
TEST_CLASS(IsJsonFileCorruptedTests)
|
||||
{
|
||||
public:
|
||||
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
|
||||
TEST_METHOD(CleanJsonFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
|
||||
TEST_METHOD(EmptyFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"empty.json", "");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
|
||||
TEST_METHOD(FileWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"corrupted.json", corrupted);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
|
||||
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
|
||||
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> allNulls(1024, '\0');
|
||||
dir.WriteFileBytes(L"workspaces.json", allNulls);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: path that does not exist returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
|
||||
TEST_METHOD(NonExistentFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
|
||||
// with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
|
||||
TEST_METHOD(LargeCleanFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string largeContent(8192, 'x');
|
||||
dir.WriteFile(L"large.json", largeContent);
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
|
||||
// chunk boundary is still detected.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
|
||||
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string content(5000, 'x');
|
||||
content[4999] = '\0';
|
||||
std::vector<char> bytes(content.begin(), content.end());
|
||||
dir.WriteFileBytes(L"sneaky.json", bytes);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(BackupConfigFilesTests)
|
||||
{
|
||||
public:
|
||||
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
|
||||
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
|
||||
// is_regular_file && extension == ".json" branch.
|
||||
// Setup: Two root-level JSON files.
|
||||
TEST_METHOD(BackupCopiesRootJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: .json files inside module subdirectories are
|
||||
// copied to ConfigBackup/<module>/.
|
||||
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
|
||||
// module directory_iterator with extension filter.
|
||||
// Setup: Root JSON + two module directories with JSON files.
|
||||
TEST_METHOD(BackupCopiesModuleJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(std::string(R"({"zones":[]})"),
|
||||
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files at root level are not copied.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
|
||||
// Setup: One JSON file + one .log file at root.
|
||||
TEST_METHOD(BackupSkipsNonJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"debug.log", "log data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
|
||||
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
|
||||
// Setup: Root JSON + Updates directory containing a file.
|
||||
TEST_METHOD(BackupSkipsUpdatesDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: running backup twice overwrites the previous
|
||||
// backup with current file content.
|
||||
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
|
||||
// copy_options::overwrite_existing.
|
||||
// Setup: Backup, modify original, backup again.
|
||||
TEST_METHOD(BackupOverwritesPreviousBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Update the original
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files inside module subdirectories
|
||||
// (e.g., FancyZones/zones.dat) should NOT be backed up.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
|
||||
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: empty root directory with no files produces
|
||||
// an empty ConfigBackup dir without errors.
|
||||
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
|
||||
TEST_METHOD(BackupEmptyRootDirSucceeds)
|
||||
{
|
||||
TempDir dir;
|
||||
// Root dir exists but has no files
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(RestoreCorruptedConfigsTests)
|
||||
{
|
||||
public:
|
||||
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
|
||||
// fs::exists + IsJsonFileCorrupted + backup integrity check.
|
||||
// Setup: Good file -> backup -> corrupt original -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedRootFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"theme":"dark"})";
|
||||
dir.WriteFile(L"settings.json", goodContent);
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the original
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"settings.json", corrupted);
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
|
||||
// moduleBackupEntry restore with integrity check.
|
||||
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedModuleFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"workspaces":[]})";
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the module file
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
|
||||
// overwritten by backup — preserves user changes made after backup.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
|
||||
// returns false, copy_file is skipped.
|
||||
// Setup: File -> backup -> modify (but keep valid) -> restore.
|
||||
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Modify original (but keep it clean JSON)
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT have been restored since it's not corrupted
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
|
||||
// restore silently does nothing (no crash, no data loss).
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
|
||||
// early return.
|
||||
// Setup: File with no prior backup.
|
||||
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
|
||||
// No backup was created - restore should silently do nothing
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
|
||||
// some corrupted and some clean, verifying selective restore.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
|
||||
// branches, selective restore based on corruption status.
|
||||
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
|
||||
TEST_METHOD(FullBackupAndRestoreRoundTrip)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Set up a realistic config structure
|
||||
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt some files (simulating #46179 scenario)
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
|
||||
// Leave FancyZones and KBM clean
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Corrupted files should be restored
|
||||
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be unchanged
|
||||
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the original file has been deleted
|
||||
// (not corrupted), restore should NOT recreate it from backup. The installer
|
||||
// may have intentionally removed obsolete config files.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
|
||||
TEST_METHOD(RestoreSkipsDeletedOriginals)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"obsolete.json", R"({"old":true})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Installer deletes the file
|
||||
std::error_code ec;
|
||||
fs::remove(dir.path() / L"obsolete.json", ec);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT be recreated
|
||||
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
|
||||
// (e.g., disk error during backup), restore should NOT copy corrupted
|
||||
// backup over the original — that would make things worse.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
|
||||
TEST_METHOD(RestoreSkipsCorruptedBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt BOTH the original AND the backup
|
||||
std::vector<char> nulls(50, '\0');
|
||||
dir.WriteFileBytes(L"settings.json", nulls);
|
||||
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Original should still be corrupted — we don't restore from bad backup
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Simulates what actually happens during a PowerToys upgrade:
|
||||
// 1. User has settings from normal use
|
||||
// 2. Updater backs up before install (Stage 1)
|
||||
// 3. Installer runs and corrupts some files (simulated)
|
||||
// 4. Updater restores corrupted files (Stage 2)
|
||||
// 5. PT relaunches and finds working configs
|
||||
TEST_CLASS(UpgradeSimulationTests)
|
||||
{
|
||||
public:
|
||||
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
|
||||
// Verifies that corrupted files are restored and clean files are untouched.
|
||||
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
|
||||
// end-to-end with 5 modules, 2 corrupted, 3 clean.
|
||||
// Setup: Realistic config structure with multiple modules.
|
||||
TEST_METHOD(SimulateUpgradeWithCorruption)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// === User's real config state before upgrade ===
|
||||
dir.WriteFile(L"settings.json",
|
||||
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json",
|
||||
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json",
|
||||
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json",
|
||||
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
|
||||
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
|
||||
R"({"machineKey":"abc123","connectToAll":true})");
|
||||
|
||||
// Non-JSON files that should be left alone
|
||||
dir.WriteFile(L"update.log", "2026-04-11 update started");
|
||||
|
||||
// === Stage 1: Backup before killing PT ===
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Verify backup was created correctly
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
|
||||
|
||||
// === Installer runs: some files get corrupted (the #46179 scenario) ===
|
||||
// Workspaces JSON filled with null bytes
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
|
||||
// Main settings partially corrupted (null bytes injected)
|
||||
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"settings.json", partialCorrupt);
|
||||
|
||||
// FancyZones, KBM, and MWB survive the install fine
|
||||
// (this is realistic - not all files get corrupted)
|
||||
|
||||
// === Stage 2: Restore after install completes ===
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// === Verify: PT relaunches and finds working configs ===
|
||||
|
||||
// Corrupted files should be restored from backup
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
|
||||
dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be untouched (not overwritten with backup)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
|
||||
dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
|
||||
dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
|
||||
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests upgrade from an old version that has fewer modules than the new version.
|
||||
// Verifies that new module configs (created by the installer) are not touched
|
||||
// by restore, while corrupted old configs are restored.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
|
||||
// has no corresponding backup entry.
|
||||
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
|
||||
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Old version had fewer modules - only settings.json
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// New installer creates new module dirs that didn't exist before
|
||||
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
|
||||
|
||||
// Old settings get corrupted during upgrade
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Old settings restored
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
|
||||
// New module settings untouched (no backup existed for them)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"enabled":true})"),
|
||||
dir.ReadFile(L"NewModule\\settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
|
||||
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
|
||||
TEST_CLASS(UpdateLifecycleTests)
|
||||
{
|
||||
public:
|
||||
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
|
||||
// and install directory — all three components needed for Stage 2.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
|
||||
// Setup: Typical paths with spaces (Program Files).
|
||||
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Must contain the stage 2 flag
|
||||
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
|
||||
// Must contain the installer path (quoted)
|
||||
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
|
||||
// Must contain the install directory (quoted) — this was MISSING before our fix
|
||||
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
|
||||
// survive CommandLineToArgvW parsing when paths contain spaces.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
|
||||
// Setup: Installer path with spaces.
|
||||
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\path with spaces\\installer.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Count quotes — should have 4 (open/close for each path)
|
||||
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
|
||||
Assert::AreEqual(size_t{ 4 }, quoteCount);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
|
||||
// Setup: Standard install path without trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: trailing backslash does not produce double
|
||||
// backslash (e.g., "...PowerToys\\PowerToys.exe").
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
|
||||
// Setup: Install path with trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
|
||||
// Setup: Empty install directory string.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"");
|
||||
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
|
||||
// the install directory (argCount >= 4), false otherwise.
|
||||
// This is the gate that prevents relaunch when using an old Stage 1
|
||||
// that didn't pass the install dir (#42004/#43011/#44071).
|
||||
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
|
||||
TEST_METHOD(CanRelaunchReflectsArgCount)
|
||||
{
|
||||
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
|
||||
|
||||
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
|
||||
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
|
||||
// Verifies quoting is correct so paths with spaces survive the round trip.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
|
||||
// Setup: Realistic paths with spaces and version numbers.
|
||||
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
|
||||
{
|
||||
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
|
||||
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
|
||||
|
||||
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
|
||||
|
||||
// Simulate what Windows does: prepend a fake exe name and parse
|
||||
std::wstring commandLine = L"PowerToys.Update.exe " + args;
|
||||
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
|
||||
Assert::IsNotNull(argv);
|
||||
Assert::AreEqual(4, argc);
|
||||
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
|
||||
Assert::AreEqual(installerPath, std::wstring(argv[2]));
|
||||
Assert::AreEqual(installDir, std::wstring(argv[3]));
|
||||
|
||||
LocalFree(argv);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UpdatingUnitTests</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Updating.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="UpdatingTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -1,5 +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.
|
||||
|
||||
#include "pch.h"
|
||||
@@ -1,17 +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.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
#include <atomic>
|
||||
#include <Windows.h>
|
||||
|
||||
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26466)
|
||||
#include "CppUnitTest.h"
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif //PCH_H
|
||||
@@ -1,170 +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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
|
||||
inline bool IsJsonFileCorrupted(const fs::path& filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t c_readChunkSize{ 4096 };
|
||||
char buffer[c_readChunkSize];
|
||||
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
|
||||
{
|
||||
const auto bytesRead = file.gcount();
|
||||
for (std::streamsize i = 0; i < bytesRead; ++i)
|
||||
{
|
||||
if (buffer[i] == '\0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup all JSON config files before update to protect against corruption (#46179)
|
||||
inline void BackupConfigFiles(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(backupDir, ec);
|
||||
// Note: remove_all failure means stale backup may persist; continue anyway
|
||||
// since create_directories will overlay
|
||||
fs::create_directories(backupDir, ec);
|
||||
if (ec)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(rootPath, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file() && entry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
else if (entry.is_directory())
|
||||
{
|
||||
const auto dirName = entry.path().filename().wstring();
|
||||
if (dirName == L"ConfigBackup" || dirName == L"Updates")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto moduleBackup = backupDir / entry.path().filename();
|
||||
fs::create_directories(moduleBackup, ec);
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
|
||||
// Restore JSON configs from backup if corruption is detected after update
|
||||
inline void RestoreCorruptedConfigs(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
if (!fs::exists(backupDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalPath = rootPath / backupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
|
||||
{
|
||||
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
}
|
||||
else if (backupEntry.is_directory())
|
||||
{
|
||||
const auto moduleDir = rootPath / backupEntry.path().filename();
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
|
||||
{
|
||||
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Build the command-line arguments for Stage 2.
|
||||
// Stage 1 passes the installer path and the PT install directory
|
||||
// so Stage 2 can run the installer and relaunch PowerToys afterward.
|
||||
// Note: paths containing embedded double-quote characters are not supported.
|
||||
// This is safe because install paths come from get_module_folderpath().
|
||||
inline std::wstring BuildStage2Arguments(
|
||||
const std::wstring& stage2Flag,
|
||||
const fs::path& installerPath,
|
||||
const fs::path& installDir)
|
||||
{
|
||||
std::wstring arguments{ stage2Flag };
|
||||
arguments += L" \"";
|
||||
arguments += installerPath.c_str();
|
||||
arguments += L"\" \"";
|
||||
arguments += installDir.c_str();
|
||||
arguments += L"\"";
|
||||
return arguments;
|
||||
}
|
||||
|
||||
// Build the full path to PowerToys.exe from the install directory.
|
||||
// Used by Stage 2 to relaunch PT after a successful update.
|
||||
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
|
||||
{
|
||||
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
|
||||
}
|
||||
|
||||
// Determine whether Stage 2 has enough information to relaunch PT.
|
||||
// Returns true if the install directory argument was provided.
|
||||
inline bool CanRelaunchAfterUpdate(int argCount)
|
||||
{
|
||||
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
|
||||
return argCount >= 4;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_98_0" displayName="$(string.SUPPORTED_POWERTOYS_0_98_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_99_0" displayName="$(string.SUPPORTED_POWERTOYS_0_99_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
@@ -152,7 +153,7 @@
|
||||
</policy>
|
||||
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_99_0" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_98_0">PowerToys version 0.98.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_99_0">PowerToys version 0.99.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
|
||||
|
||||
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
||||
|
||||
@@ -3,16 +3,101 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PowerOCR"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
Exit="Application_Exit"
|
||||
ShutdownMode="OnExplicitShutdown"
|
||||
Startup="Application_Startup">
|
||||
Startup="Application_Startup"
|
||||
ThemeMode="System">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemesDictionary Theme="Dark" />
|
||||
<ui:ControlsDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
<Style
|
||||
x:Key="SubtleButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Focusable="False"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SubtleToggleButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type ToggleButton}}"
|
||||
TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Focusable="False"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<Window
|
||||
<Window
|
||||
x:Class="PowerOCR.OCROverlay"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:p="clr-namespace:PowerOCR.Properties"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
x:Name="TextExtractorWindow"
|
||||
Title="TextExtractor"
|
||||
ui:Design.Background="Transparent"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Loaded="Window_Loaded"
|
||||
@@ -22,26 +20,6 @@
|
||||
WindowStyle="None"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.Resources>
|
||||
<Style BasedOn="{StaticResource DefaultToggleButtonStyle}" TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Margin" Value="4,0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="{x:Type Button}">
|
||||
<Setter Property="Margin" Value="4,0" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Viewbox>
|
||||
<Image x:Name="BackgroundImage" Stretch="UniformToFill" />
|
||||
@@ -101,17 +79,11 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
d:Visibility="Visible"
|
||||
Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Visibility="Collapsed">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="32"
|
||||
Opacity="0.28"
|
||||
RenderingBias="Performance"
|
||||
ShadowDepth="1" />
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
@@ -133,35 +105,67 @@
|
||||
</ComboBox>
|
||||
<ToggleButton
|
||||
x:Name="SingleLineToggleButton"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="4,0"
|
||||
Padding="0"
|
||||
d:IsChecked="True"
|
||||
AutomationProperties.Name="{x:Static p:Resources.ResultTextSingleLine}"
|
||||
Click="SingleLineMenuItem_Click"
|
||||
IsChecked="{Binding IsChecked, ElementName=SingleLineMenuItem, Mode=TwoWay}"
|
||||
Style="{DynamicResource SubtleToggleButtonStyle}"
|
||||
ToolTip="{x:Static p:Resources.ResultTextSingleLineShortcut}">
|
||||
<ui:SymbolIcon FontSize="18" Symbol="SubtractSquare24" />
|
||||
<TextBlock
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Text="" />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
x:Name="TableToggleButton"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="4,0"
|
||||
Padding="0"
|
||||
d:IsChecked="True"
|
||||
AutomationProperties.Name="{x:Static p:Resources.ResultTextTable}"
|
||||
Click="TableToggleButton_Click"
|
||||
IsChecked="{Binding IsChecked, ElementName=TableMenuItem, Mode=TwoWay}"
|
||||
Style="{DynamicResource SubtleToggleButtonStyle}"
|
||||
ToolTip="{x:Static p:Resources.ResultTextTableShortcut}">
|
||||
<ui:SymbolIcon FontSize="18" Symbol="Table24" />
|
||||
<TextBlock
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Text="" />
|
||||
</ToggleButton>
|
||||
<Button
|
||||
x:Name="SettingsButton"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="4,0"
|
||||
Padding="0"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Settings}"
|
||||
Click="SettingsMenuItem_Click"
|
||||
Style="{DynamicResource SubtleButtonStyle}"
|
||||
ToolTip="{x:Static p:Resources.Settings}">
|
||||
<ui:SymbolIcon FontSize="18" Symbol="Settings24" />
|
||||
<TextBlock
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Text="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="4,0"
|
||||
Padding="0"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Cancel}"
|
||||
Click="CancelMenuItem_Click"
|
||||
Style="{DynamicResource SubtleButtonStyle}"
|
||||
ToolTip="{x:Static p:Resources.CancelShortcut}">
|
||||
<ui:SymbolIcon FontSize="18" Symbol="Dismiss24" />
|
||||
<TextBlock
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Text="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -64,8 +64,6 @@ public partial class OCROverlay : Window
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, Wpf.Ui.Controls.WindowBackdropType.None);
|
||||
|
||||
PopulateLanguageMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Composition" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="WPF-UI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -252,11 +252,13 @@ void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
}
|
||||
|
||||
Sound::Type soundType = Sound::Type::Off;
|
||||
bool stateChanged = false;
|
||||
bool topmost = IsTopmost(window);
|
||||
if (topmost)
|
||||
{
|
||||
if (UnpinTopmostWindow(window))
|
||||
{
|
||||
stateChanged = true;
|
||||
auto iter = m_topmostWindows.find(window);
|
||||
if (iter != m_topmostWindows.end())
|
||||
{
|
||||
@@ -274,6 +276,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
{
|
||||
if (PinTopmostWindow(window))
|
||||
{
|
||||
stateChanged = true;
|
||||
soundType = Sound::Type::On;
|
||||
AssignBorder(window);
|
||||
|
||||
@@ -281,7 +284,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
}
|
||||
}
|
||||
|
||||
if (AlwaysOnTopSettings::settings()->enableSound)
|
||||
if (stateChanged && AlwaysOnTopSettings::settings()->enableSound)
|
||||
{
|
||||
m_sound.Play(soundType);
|
||||
}
|
||||
|
||||
57
src/modules/cmdpal/CommandPalette - no UI tests.slnf
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"solution": {
|
||||
"path": "..\\..\\..\\PowerToys.slnx",
|
||||
"projects": [
|
||||
"src\\common\\CalculatorEngineCommon\\CalculatorEngineCommon.vcxproj",
|
||||
"src\\common\\Common.UI.Controls\\Common.UI.Controls.csproj",
|
||||
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
|
||||
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
|
||||
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
|
||||
"src\\common\\interop\\PowerToys.Interop.vcxproj",
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Common.UnitTests\\Microsoft.CmdPal.Common.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Apps.UnitTests\\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Registry.UnitTests\\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Shell.UnitTests\\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.System.UnitTests\\Microsoft.CmdPal.Ext.System.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.UnitTestsBase\\Microsoft.CmdPal.Ext.UnitTestBase.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Apps\\Microsoft.CmdPal.Ext.Apps.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Bookmark\\Microsoft.CmdPal.Ext.Bookmarks.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Calc\\Microsoft.CmdPal.Ext.Calc.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.ClipboardHistory\\Microsoft.CmdPal.Ext.ClipboardHistory.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Indexer\\Microsoft.CmdPal.Ext.Indexer.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.PerformanceMonitor\\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Registry\\Microsoft.CmdPal.Ext.Registry.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.RemoteDesktop\\Microsoft.CmdPal.Ext.RemoteDesktop.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Shell\\Microsoft.CmdPal.Ext.Shell.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.System\\Microsoft.CmdPal.Ext.System.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.TimeDate\\Microsoft.CmdPal.Ext.TimeDate.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WebSearch\\Microsoft.CmdPal.Ext.WebSearch.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WinGet\\Microsoft.CmdPal.Ext.WinGet.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowWalker\\Microsoft.CmdPal.Ext.WindowWalker.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsServices\\Microsoft.CmdPal.Ext.WindowsServices.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsSettings\\Microsoft.CmdPal.Ext.WindowsSettings.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsTerminal\\Microsoft.CmdPal.Ext.WindowsTerminal.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\ProcessMonitorExtension\\ProcessMonitorExtension.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,14 @@
|
||||
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
|
||||
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
|
||||
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
|
||||
"src\\common\\SettingsAPI\\SettingsAPI.vcxproj",
|
||||
"src\\common\\UITestAutomation\\UITestAutomation.csproj",
|
||||
"src\\common\\interop\\PowerToys.Interop.vcxproj",
|
||||
"src\\common\\logger\\logger.vcxproj",
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\logging\\logging.vcxproj",
|
||||
"src\\modules\\MouseUtils\\MouseJump.Common\\MouseJump.Common.csproj",
|
||||
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
|
||||
@@ -51,7 +57,9 @@
|
||||
"src\\modules\\cmdpal\\ext\\ProcessMonitorExtension\\ProcessMonitorExtension.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj"
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj",
|
||||
"src\\modules\\powerdisplay\\PowerDisplay.Models\\PowerDisplay.Models.csproj",
|
||||
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,7 @@ public record DockSettings
|
||||
{
|
||||
public DockSide Side { get; init; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; init; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; init; } = DockSize.Small;
|
||||
public DockSize DockSize { get; init; } = DockSize.Default;
|
||||
|
||||
public bool AlwaysOnTop { get; set; } = true;
|
||||
|
||||
@@ -139,9 +137,8 @@ public enum DockSide
|
||||
|
||||
public enum DockSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
Default,
|
||||
Compact,
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
|
||||
@@ -17,12 +17,9 @@
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<StackLayout
|
||||
x:Key="ItemsOrientationLayout"
|
||||
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
|
||||
Spacing="4" />
|
||||
<StackLayout x:Key="ItemsOrientationLayout" Orientation="{x:Bind ItemsOrientation, Mode=OneWay}" />
|
||||
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
|
||||
<StackPanel Orientation="Vertical" Spacing="4" />
|
||||
@@ -76,7 +73,7 @@
|
||||
|
||||
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,0,4,0" />
|
||||
<Setter Property="Margin" Value="0,0,0,0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
@@ -209,13 +206,13 @@
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="Transparent"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<!-- Dock content with Start / Center / End sections -->
|
||||
<local:DockContentControl
|
||||
x:Name="ContentGrid"
|
||||
Margin="4"
|
||||
Background="Transparent"
|
||||
IsEditMode="{x:Bind IsEditMode, Mode=OneWay}"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<local:DockContentControl.StartSource>
|
||||
@@ -247,7 +244,6 @@
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</local:DockContentControl.StartActionButton>
|
||||
|
||||
<local:DockContentControl.CenterSource>
|
||||
<ListView
|
||||
x:Name="CenterListView"
|
||||
@@ -282,6 +278,8 @@
|
||||
<ListView
|
||||
x:Name="EndListView"
|
||||
MinWidth="48"
|
||||
MinHeight="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
DragEnter="BandListView_DragEnter"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
@@ -311,7 +309,6 @@
|
||||
</Button>
|
||||
</local:DockContentControl.EndActionButton>
|
||||
</local:DockContentControl>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="EditButtonsTeachingTip"
|
||||
MinWidth="0"
|
||||
@@ -344,7 +341,7 @@
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.Margin" Value="4,0,4,4" />
|
||||
<Setter Target="ContentGrid.Margin" Value="4,0,4,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnBottom">
|
||||
@@ -391,6 +388,25 @@
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
<!--
|
||||
Compact overrides: zeroes margins/borders set by DockOrientation.
|
||||
Declared after DockOrientation so its setters win when both groups
|
||||
target the same property.
|
||||
-->
|
||||
<VisualStateGroup x:Name="DockSizeStates">
|
||||
<VisualState x:Name="DefaultSize" />
|
||||
<VisualState x:Name="CompactSize">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSize, Mode=OneWay}" To="Compact" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.Margin" Value="0" />
|
||||
<Setter Target="ContentGrid.Padding" Value="0" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -47,6 +47,15 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
set => SetValue(DockSideProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DockSizeProperty =
|
||||
DependencyProperty.Register(nameof(DockSize), typeof(DockSize), typeof(DockControl), new PropertyMetadata(DockSize.Default));
|
||||
|
||||
public DockSize DockSize
|
||||
{
|
||||
get => (DockSize)GetValue(DockSizeProperty);
|
||||
set => SetValue(DockSizeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsEditModeProperty =
|
||||
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
|
||||
|
||||
@@ -234,7 +243,10 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
{
|
||||
DockSide = settings.Side;
|
||||
|
||||
// Compact mode is only supported for Top/Bottom positions
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
var effectiveSize = isHorizontal ? settings.DockSize : DockSize.Default;
|
||||
DockSize = effectiveSize;
|
||||
|
||||
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
|
||||
|
||||
@@ -290,6 +302,11 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
|
||||
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
|
||||
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
|
||||
|
||||
// Hide subtitle toggle in compact mode — no subtitle in the template
|
||||
ShowSubtitlesMenuItem.Visibility = DockSize == DockSize.Compact
|
||||
? Visibility.Collapsed
|
||||
: Visibility.Visible;
|
||||
|
||||
PreparePopupForShow(EditModeContextMenu, dockItem);
|
||||
EditModeContextMenu.ShowAt(
|
||||
dockItem,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<CornerRadius x:Key="DockItemCornerRadius">4</CornerRadius>
|
||||
<Thickness x:Key="DockItemPadding">4,0,4,0</Thickness>
|
||||
|
||||
<Thickness x:Key="DockItemMargin">2,0,2,0</Thickness>
|
||||
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
|
||||
|
||||
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
|
||||
@@ -60,12 +60,13 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:DockItemControl">
|
||||
<Grid x:Name="PART_HitTestGrid" Background="Transparent">
|
||||
<Grid
|
||||
x:Name="PART_RootGrid"
|
||||
Padding="{StaticResource DockItemMargin}"
|
||||
Background="Transparent">
|
||||
<Grid
|
||||
x:Name="PART_RootGrid"
|
||||
x:Name="PART_BackPlate"
|
||||
MinWidth="32"
|
||||
MinHeight="30"
|
||||
Margin="{TemplateBinding InnerMargin}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
@@ -128,20 +129,20 @@
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
|
||||
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
|
||||
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
@@ -192,6 +193,16 @@
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CompactStates">
|
||||
<VisualState x:Name="DefaultLayout" />
|
||||
<VisualState x:Name="Compact">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Padding" Value="0" />
|
||||
<Setter Target="SubtitleText.Visibility" Value="Collapsed" />
|
||||
<Setter Target="TitleText.Margin" Value="0,-1,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
@@ -84,12 +84,35 @@ public sealed partial class DockItemControl : Control
|
||||
set => SetValue(TextVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsCompactProperty =
|
||||
DependencyProperty.Register(nameof(IsCompact), typeof(bool), typeof(DockItemControl), new PropertyMetadata(false, OnIsCompactPropertyChanged));
|
||||
|
||||
public bool IsCompact
|
||||
{
|
||||
get => (bool)GetValue(IsCompactProperty);
|
||||
set => SetValue(IsCompactProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsCompactPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateCompactState();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCompactState()
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsCompact ? "Compact" : "DefaultLayout", true);
|
||||
}
|
||||
|
||||
private const string IconPresenterName = "IconPresenter";
|
||||
|
||||
private FrameworkElement? _iconPresenter;
|
||||
private DockControl? _parentDock;
|
||||
private ToolTip? _toolTip;
|
||||
private long _dockSideCallbackToken = -1;
|
||||
private long _dockSizeCallbackToken = -1;
|
||||
|
||||
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
@@ -122,6 +145,14 @@ public sealed partial class DockItemControl : Control
|
||||
|
||||
private void UpdateTextVisibilityState()
|
||||
{
|
||||
// When TextVisibility is Collapsed, always hide text and collapse the
|
||||
// grid column/spacing so the icon-only layout doesn't waste space.
|
||||
if (TextVisibility == Visibility.Collapsed)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "TextHidden", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine which visual state to use based on title/subtitle presence
|
||||
var stateName = (HasTitle, HasSubtitle) switch
|
||||
{
|
||||
@@ -184,6 +215,7 @@ public sealed partial class DockItemControl : Control
|
||||
UpdateIconVisibility();
|
||||
UpdateToolTip();
|
||||
UpdateAlignment();
|
||||
UpdateCompactState();
|
||||
}
|
||||
|
||||
private void UpdateToolTip()
|
||||
@@ -249,10 +281,14 @@ public sealed partial class DockItemControl : Control
|
||||
{
|
||||
_parentDock = dock;
|
||||
UpdateInnerMarginForDockSide(dock.DockSide);
|
||||
UpdateCompactFromParent(dock);
|
||||
UpdateAllVisibility();
|
||||
_dockSideCallbackToken = dock.RegisterPropertyChangedCallback(
|
||||
DockControl.DockSideProperty,
|
||||
OnParentDockSideChanged);
|
||||
_dockSizeCallbackToken = dock.RegisterPropertyChangedCallback(
|
||||
DockControl.DockSizeProperty,
|
||||
OnParentDockSizeChanged);
|
||||
}
|
||||
|
||||
UpdateToolTip();
|
||||
@@ -266,12 +302,24 @@ public sealed partial class DockItemControl : Control
|
||||
|
||||
private void DockItemControl_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_parentDock is not null && _dockSideCallbackToken >= 0)
|
||||
if (_parentDock is not null)
|
||||
{
|
||||
_parentDock.UnregisterPropertyChangedCallback(
|
||||
DockControl.DockSideProperty,
|
||||
_dockSideCallbackToken);
|
||||
_dockSideCallbackToken = -1;
|
||||
if (_dockSideCallbackToken >= 0)
|
||||
{
|
||||
_parentDock.UnregisterPropertyChangedCallback(
|
||||
DockControl.DockSideProperty,
|
||||
_dockSideCallbackToken);
|
||||
_dockSideCallbackToken = -1;
|
||||
}
|
||||
|
||||
if (_dockSizeCallbackToken >= 0)
|
||||
{
|
||||
_parentDock.UnregisterPropertyChangedCallback(
|
||||
DockControl.DockSizeProperty,
|
||||
_dockSizeCallbackToken);
|
||||
_dockSizeCallbackToken = -1;
|
||||
}
|
||||
|
||||
_parentDock = null;
|
||||
}
|
||||
|
||||
@@ -283,11 +331,24 @@ public sealed partial class DockItemControl : Control
|
||||
{
|
||||
if (sender is DockControl dock)
|
||||
{
|
||||
UpdateInnerMarginForDockSide(dock.DockSide);
|
||||
// UpdateInnerMarginForDockSide(dock.DockSide);
|
||||
UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnParentDockSizeChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
if (sender is DockControl dock)
|
||||
{
|
||||
UpdateCompactFromParent(dock);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCompactFromParent(DockControl dock)
|
||||
{
|
||||
IsCompact = dock.DockSize == DockSize.Compact;
|
||||
}
|
||||
|
||||
private void UpdateInnerMarginForDockSide(DockSide side)
|
||||
{
|
||||
// Push the visual (PART_RootGrid) inward on the screen-edge side so
|
||||
@@ -296,7 +357,7 @@ public sealed partial class DockItemControl : Control
|
||||
// DockControl's ContentGrid on the screen-edge side.
|
||||
InnerMargin = side switch
|
||||
{
|
||||
DockSide.Top => new Thickness(0, 4, 0, 0),
|
||||
DockSide.Top => new Thickness(0, 0, 0, 0),
|
||||
DockSide.Bottom => new Thickness(0, 0, 0, 4),
|
||||
DockSide.Left => new Thickness(8, 0, 0, 0),
|
||||
DockSide.Right => new Thickness(0, 0, 8, 0),
|
||||
|
||||
@@ -13,9 +13,8 @@ internal static class DockSettingsToViews
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 128,
|
||||
DockSize.Medium => 192,
|
||||
DockSize.Large => 256,
|
||||
DockSize.Default => 86,
|
||||
DockSize.Compact => 86,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
@@ -24,9 +23,8 @@ internal static class DockSettingsToViews
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 38,
|
||||
DockSize.Medium => 54,
|
||||
DockSize.Large => 76,
|
||||
DockSize.Default => 38,
|
||||
DockSize.Compact => 24,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed partial class DockWindow : WindowEx,
|
||||
_settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
_settingsService.SettingsChanged += SettingsChangedHandler;
|
||||
_settings = mainSettings.DockSettings;
|
||||
_lastSize = _settings.DockSize;
|
||||
_lastSize = EffectiveDockSize(_settings);
|
||||
|
||||
viewModel = serviceProvider.GetService<DockViewModel>()!;
|
||||
_themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
@@ -174,7 +174,7 @@ public sealed partial class DockWindow : WindowEx,
|
||||
if (_appBarData.hWnd != IntPtr.Zero)
|
||||
{
|
||||
var sameEdge = _appBarData.uEdge == side;
|
||||
var sameSize = _lastSize == _settings.DockSize;
|
||||
var sameSize = _lastSize == EffectiveDockSize(_settings);
|
||||
if (sameEdge && sameSize)
|
||||
{
|
||||
UpdateTopmostState();
|
||||
@@ -332,7 +332,7 @@ public sealed partial class DockWindow : WindowEx,
|
||||
|
||||
// Stash the last size we created the bar at, so we know when to hot-
|
||||
// reload it
|
||||
_lastSize = _settings.DockSize;
|
||||
_lastSize = EffectiveDockSize(_settings);
|
||||
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
@@ -384,15 +384,9 @@ public sealed partial class DockWindow : WindowEx,
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
// Get system border metrics
|
||||
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
|
||||
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
|
||||
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
|
||||
|
||||
var scaleFactor = dpi / 96.0;
|
||||
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, scaleFactor);
|
||||
var effectiveSize = EffectiveDockSize(_settings);
|
||||
UpdateAppBarDataForEdge(_settings.Side, effectiveSize, scaleFactor);
|
||||
|
||||
// Query and set position
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
|
||||
@@ -406,16 +400,16 @@ public sealed partial class DockWindow : WindowEx,
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
_appBarData.rc.bottom = _appBarData.rc.top + (int)(DockSettingsToViews.HeightForSize(_settings.DockSize) * scaleFactor);
|
||||
_appBarData.rc.bottom = _appBarData.rc.top + (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
_appBarData.rc.top = _appBarData.rc.bottom - (int)(DockSettingsToViews.HeightForSize(_settings.DockSize) * scaleFactor);
|
||||
_appBarData.rc.top = _appBarData.rc.bottom - (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
|
||||
break;
|
||||
case DockSide.Left:
|
||||
_appBarData.rc.right = _appBarData.rc.left + (int)(DockSettingsToViews.WidthForSize(_settings.DockSize) * scaleFactor);
|
||||
_appBarData.rc.right = _appBarData.rc.left + (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
|
||||
break;
|
||||
case DockSide.Right:
|
||||
_appBarData.rc.left = _appBarData.rc.right - (int)(DockSettingsToViews.WidthForSize(_settings.DockSize) * scaleFactor);
|
||||
_appBarData.rc.left = _appBarData.rc.right - (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -428,23 +422,28 @@ public sealed partial class DockWindow : WindowEx,
|
||||
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
|
||||
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
|
||||
|
||||
// Account for system borders when moving the window
|
||||
// Adjust position to account for window frame/border
|
||||
var adjustedLeft = _appBarData.rc.left - frameWidth;
|
||||
var adjustedTop = _appBarData.rc.top - frameWidth;
|
||||
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
|
||||
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
|
||||
|
||||
// Move the actual window
|
||||
// The dock window is borderless (SetBorderAndTitleBar(false, false),
|
||||
// IsResizable = false) so no frame compensation is needed — the
|
||||
// app bar rect matches the window rect exactly.
|
||||
PInvoke.MoveWindow(
|
||||
_hwnd,
|
||||
adjustedLeft,
|
||||
adjustedTop,
|
||||
adjustedWidth,
|
||||
adjustedHeight,
|
||||
_appBarData.rc.left,
|
||||
_appBarData.rc.top,
|
||||
_appBarData.rc.right - _appBarData.rc.left,
|
||||
_appBarData.rc.bottom - _appBarData.rc.top,
|
||||
true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compact mode is only supported for Top/Bottom dock positions.
|
||||
/// For Left/Right, always use Default size.
|
||||
/// </summary>
|
||||
private static DockSize EffectiveDockSize(DockSettings settings)
|
||||
{
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
return isHorizontal ? settings.DockSize : DockSize.Default;
|
||||
}
|
||||
|
||||
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
|
||||
{
|
||||
Logger.LogDebug("UpdateAppBarDataForEdge");
|
||||
@@ -587,11 +586,21 @@ public sealed partial class DockWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_GETMINMAXINFO to control window size limits
|
||||
// Handle WM_GETMINMAXINFO to allow the dock to be smaller than
|
||||
// the default minimum window size (SM_CYMINTRACK ~36px).
|
||||
else if (msg == PInvoke.WM_GETMINMAXINFO)
|
||||
{
|
||||
// We can modify the min/max tracking info here if needed
|
||||
// For now, let it pass through but we could restrict max size
|
||||
// Call the original WndProc first so it fills default values,
|
||||
// then override the minimum tracking size.
|
||||
var result = PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
|
||||
unsafe
|
||||
{
|
||||
var minMaxInfo = (MINMAXINFO*)lParam.Value;
|
||||
minMaxInfo->ptMinTrackSize.X = 1;
|
||||
minMaxInfo->ptMinTrackSize.Y = 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle the AppBarMessage message
|
||||
|
||||
@@ -94,6 +94,7 @@ WM_WINDOWPOSCHANGING
|
||||
WM_SHOWWINDOW
|
||||
WM_SIZE
|
||||
WM_GETMINMAXINFO
|
||||
MINMAXINFO
|
||||
SetWinEventHook
|
||||
WINDOW_STYLE
|
||||
SC_MINIMIZE
|
||||
|
||||
@@ -67,6 +67,20 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Dock Size (only for Top/Bottom positions) -->
|
||||
<controls:SettingsCard
|
||||
x:Name="DockSizeSettingsCard"
|
||||
x:Uid="DockAppearance_DockSize_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
x:Name="DockSizeComboBox"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind SelectedDockSizeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockSize_Default" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockSize_Compact" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
|
||||
@@ -42,7 +42,9 @@ public sealed partial class DockSettingsPage : Page
|
||||
{
|
||||
// Initialize UI controls to match current settings
|
||||
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
|
||||
DockSizeComboBox.SelectedIndex = SelectedDockSizeIndex;
|
||||
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
|
||||
UpdateDockSizeCardVisibility();
|
||||
}
|
||||
|
||||
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
|
||||
@@ -108,7 +110,11 @@ public sealed partial class DockSettingsPage : Page
|
||||
public int SelectedSideIndex
|
||||
{
|
||||
get => SideToSelectedIndex(ViewModel.Dock_Side);
|
||||
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
set
|
||||
{
|
||||
ViewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
UpdateDockSizeCardVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectedBackdropIndex
|
||||
@@ -126,18 +132,16 @@ public sealed partial class DockSettingsPage : Page
|
||||
// Conversion methods for ComboBox bindings
|
||||
private static int DockSizeToSelectedIndex(DockSize size) => size switch
|
||||
{
|
||||
DockSize.Small => 0,
|
||||
DockSize.Medium => 1,
|
||||
DockSize.Large => 2,
|
||||
DockSize.Default => 0,
|
||||
DockSize.Compact => 1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static DockSize SelectedIndexToDockSize(int index) => index switch
|
||||
{
|
||||
0 => DockSize.Small,
|
||||
1 => DockSize.Medium,
|
||||
2 => DockSize.Large,
|
||||
_ => DockSize.Small,
|
||||
0 => DockSize.Default,
|
||||
1 => DockSize.Compact,
|
||||
_ => DockSize.Default,
|
||||
};
|
||||
|
||||
private static int SideToSelectedIndex(DockSide side) => side switch
|
||||
@@ -172,6 +176,13 @@ public sealed partial class DockSettingsPage : Page
|
||||
_ => DockBackdrop.Acrylic,
|
||||
};
|
||||
|
||||
private void UpdateDockSizeCardVisibility()
|
||||
{
|
||||
var side = ViewModel.Dock_Side;
|
||||
var isTopOrBottom = side == DockSide.Top || side == DockSide.Bottom;
|
||||
DockSizeSettingsCard.Visibility = isTopOrBottom ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private List<TopLevelViewModel> GetAllBands()
|
||||
{
|
||||
var allBands = new List<TopLevelViewModel>();
|
||||
|
||||
@@ -939,6 +939,18 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="DockAppearance_DockPosition_Bottom.Content" xml:space="preserve">
|
||||
<value>Bottom</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockSize_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Size</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockSize_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Choose the dock size; subtitles of dock items are hidden in compact mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockSize_Default.Content" xml:space="preserve">
|
||||
<value>Default</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockSize_Compact.Content" xml:space="preserve">
|
||||
<value>Compact</value>
|
||||
</data>
|
||||
<data name="top_level_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to home</value>
|
||||
<comment>Command name for pinning an item to the top level list of commands</comment>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Globalization;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
@@ -10,6 +11,25 @@ namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
[TestClass]
|
||||
public class QueryHelperTests
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
|
||||
originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanUp()
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("2²", "4")]
|
||||
[DataRow("2³", "8")]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.Calc.Pages;
|
||||
@@ -14,6 +15,25 @@ namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
|
||||
[TestClass]
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
private CultureInfo originalUiCulture;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
|
||||
originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanUp()
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("2+2", "4")]
|
||||
[DataRow("5*3", "15")]
|
||||
@@ -84,6 +104,40 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians, "de-DE")]
|
||||
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees, "de-DE")]
|
||||
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians, "de-DE")]
|
||||
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians, "fr-FR")]
|
||||
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees, "fr-FR")]
|
||||
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians, "fr-FR")]
|
||||
public void TrigModeSettingsTest_NonEnglishCulture(string input, string expected, CalculateEngine.TrigMode trigMode, string cultureName)
|
||||
{
|
||||
var previousCulture = CultureInfo.CurrentCulture;
|
||||
var previousUiCulture = CultureInfo.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo(cultureName, false);
|
||||
CultureInfo.CurrentUICulture = new CultureInfo(cultureName, false);
|
||||
|
||||
var settings = new Settings(trigUnit: trigMode, outputUseEnglishFormat: true);
|
||||
|
||||
var page = new CalculatorListPage(settings);
|
||||
|
||||
page.UpdateSearchText(string.Empty, input);
|
||||
var result = page.GetItems().FirstOrDefault();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct for culture '{cultureName}'. Current result: {result.Title}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = previousCulture;
|
||||
CultureInfo.CurrentUICulture = previousUiCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("2^64", "18446744073709551616", "0x10000000000000000")]
|
||||
[DataRow("0-(2^64)", "-18446744073709551616", "-0x10000000000000000")]
|
||||
|
||||
@@ -108,7 +108,6 @@ internal static class ResultHelper
|
||||
tags.Add(new Tag
|
||||
{
|
||||
Text = Resources.windowwalker_NotResponding,
|
||||
Foreground = ColorHelpers.FromRgb(220, 20, 60),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
@@ -556,7 +557,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
return true;
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
var monitorPrefix = string.IsNullOrEmpty(monitorId) ? string.Empty : $"[{monitorId}] ";
|
||||
Logger.LogError($"{monitorPrefix}Failed to read VCP 0x{vcpCode:X2}, error code: {lastError}");
|
||||
return false;
|
||||
@@ -696,8 +697,8 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace PowerDisplay.Common.Drivers
|
||||
IntPtr hMonitor,
|
||||
MonitorInfoEx* lpmi);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "EnumDisplaySettingsW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[LibraryImport("user32.dll", EntryPoint = "EnumDisplaySettingsW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static unsafe partial bool EnumDisplaySettings(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? lpszDeviceName,
|
||||
@@ -86,7 +86,7 @@ namespace PowerDisplay.Common.Drivers
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool DestroyPhysicalMonitor(IntPtr hMonitor);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[LibraryImport("Dxva2.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetVCPFeatureAndVCPFeatureReply(
|
||||
IntPtr hPhysicalMonitor,
|
||||
@@ -95,7 +95,7 @@ namespace PowerDisplay.Common.Drivers
|
||||
out uint pdwCurrentValue,
|
||||
out uint pdwMaximumValue);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[LibraryImport("Dxva2.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetVCPFeature(
|
||||
IntPtr hPhysicalMonitor,
|
||||
@@ -114,9 +114,5 @@ namespace PowerDisplay.Common.Drivers
|
||||
IntPtr hPhysicalMonitor,
|
||||
IntPtr pszASCIICapabilitiesString,
|
||||
uint dwCapabilitiesStringLengthInCharacters);
|
||||
|
||||
// ==================== Kernel32.dll ====================
|
||||
[LibraryImport("kernel32.dll")]
|
||||
internal static partial uint GetLastError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,13 +210,16 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
inParams,
|
||||
out WmiMethodParameters outParams);
|
||||
|
||||
// Check return value (0 indicates success)
|
||||
if (result == 0)
|
||||
using (outParams)
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
// Check return value (0 indicates success)
|
||||
if (result == 0)
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
|
||||
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +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 PowerDisplay.Common.Utils;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.Common.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="CustomVcpValueMapping"/> that provide display-related properties.
|
||||
/// These depend on <see cref="VcpNames"/> and are therefore kept in PowerDisplay.Lib
|
||||
/// rather than in the shared PowerDisplay.Models project.
|
||||
/// </summary>
|
||||
public static class CustomVcpValueMappingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display name for the VCP code (e.g., "Select Color Preset").
|
||||
/// </summary>
|
||||
public static string GetVcpCodeDisplayName(this CustomVcpValueMapping mapping)
|
||||
{
|
||||
return VcpNames.GetCodeName(mapping.VcpCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted display name for the VCP value (e.g., "6500K (0x05)").
|
||||
/// </summary>
|
||||
public static string GetValueDisplayName(this CustomVcpValueMapping mapping)
|
||||
{
|
||||
return VcpNames.GetFormattedValueName(mapping.VcpCode, mapping.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary string for display in the UI list.
|
||||
/// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
|
||||
/// </summary>
|
||||
public static string GetDisplaySummary(this CustomVcpValueMapping mapping)
|
||||
{
|
||||
var baseSummary = $"{VcpNames.GetValueName(mapping.VcpCode, mapping.Value) ?? $"0x{mapping.Value:X2}"} \u2192 {mapping.CustomName}";
|
||||
if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorName))
|
||||
{
|
||||
return $"{baseSummary} ({mapping.TargetMonitorName})";
|
||||
}
|
||||
|
||||
return baseSummary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ namespace PowerDisplay.Common.Interfaces
|
||||
/// <summary>
|
||||
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
|
||||
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
|
||||
/// Use ColorTemperatureHelper to convert to/from human-readable display names.
|
||||
/// Use VcpNames to convert to/from human-readable display names.
|
||||
/// </summary>
|
||||
int ColorTemperatureVcp { get; set; }
|
||||
|
||||
|
||||
@@ -1,29 +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 PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.Common.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for profile management service.
|
||||
/// Provides abstraction for loading and saving PowerDisplay profiles.
|
||||
/// Enables dependency injection and unit testing.
|
||||
/// </summary>
|
||||
public interface IProfileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads PowerDisplay profiles from disk.
|
||||
/// </summary>
|
||||
/// <returns>PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails.</returns>
|
||||
PowerDisplayProfiles LoadProfiles();
|
||||
|
||||
/// <summary>
|
||||
/// Saves PowerDisplay profiles to disk.
|
||||
/// </summary>
|
||||
/// <param name="profiles">The profiles collection to save.</param>
|
||||
/// <returns>True if save was successful, false otherwise.</returns>
|
||||
bool SaveProfiles(PowerDisplayProfiles profiles);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Common.Models;
|
||||
using static PowerDisplay.Common.Drivers.NativeConstants;
|
||||
@@ -63,9 +64,9 @@ namespace PowerDisplay.Common.Services
|
||||
|
||||
if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode))
|
||||
{
|
||||
var error = GetLastError();
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
Logger.LogError($"SetRotation: EnumDisplaySettings failed for {gdiDeviceName}, error: {error}");
|
||||
return MonitorOperationResult.Failure($"Failed to get current display settings for {gdiDeviceName}", (int)error);
|
||||
return MonitorOperationResult.Failure($"Failed to get current display settings for {gdiDeviceName}", error);
|
||||
}
|
||||
|
||||
int currentOrientation = devMode.DmDisplayOrientation;
|
||||
@@ -144,8 +145,9 @@ namespace PowerDisplay.Common.Services
|
||||
|
||||
return devMode.DmDisplayOrientation;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"GetCurrentOrientation: Exception for {gdiDeviceName}: {ex.Message}");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ namespace PowerDisplay.Common.Services
|
||||
private readonly ConcurrentDictionary<string, MonitorState> _states = new();
|
||||
private readonly SimpleDebouncer _saveDebouncer;
|
||||
|
||||
private bool _disposed;
|
||||
private bool _isDirty; // Track pending changes for flush on dispose
|
||||
private volatile bool _disposed;
|
||||
private volatile bool _isDirty; // Track pending changes for flush on dispose
|
||||
private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,52 +2,22 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using PowerDisplay.Common.Interfaces;
|
||||
using PowerDisplay.Models;
|
||||
using ModelsProfileHelper = PowerDisplay.Models.ProfileHelper;
|
||||
|
||||
namespace PowerDisplay.Common.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin facade over <see cref="ProfileHelper"/> that satisfies <see cref="IProfileService"/>
|
||||
/// and provides named static entry points for PowerDisplay.exe callers.
|
||||
/// Thin facade over <see cref="ProfileHelper"/> that provides named static entry points
|
||||
/// for PowerDisplay.exe callers.
|
||||
/// All locking and compound-operation atomicity is handled by <see cref="PowerDisplay.Models.ProfileHelper"/>.
|
||||
/// </summary>
|
||||
public class ProfileService : IProfileService
|
||||
public static class ProfileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the singleton instance of the ProfileService.
|
||||
/// </summary>
|
||||
public static IProfileService Instance { get; } = new ProfileService();
|
||||
|
||||
private ProfileService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.LoadProfiles"/>
|
||||
public static PowerDisplayProfiles LoadProfiles() => ModelsProfileHelper.LoadProfiles();
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.SaveProfiles"/>
|
||||
public static bool SaveProfiles(PowerDisplayProfiles profiles) => ModelsProfileHelper.SaveProfiles(profiles);
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.AddOrUpdateProfile"/>
|
||||
public static bool AddOrUpdateProfile(PowerDisplayProfile profile) => ModelsProfileHelper.AddOrUpdateProfile(profile);
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.RenameAndUpdateProfile"/>
|
||||
public static bool RenameAndUpdateProfile(string oldName, PowerDisplayProfile newProfile) => ModelsProfileHelper.RenameAndUpdateProfile(oldName, newProfile);
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.RemoveProfile"/>
|
||||
public static bool RemoveProfile(string profileName) => ModelsProfileHelper.RemoveProfile(profileName);
|
||||
|
||||
/// <inheritdoc cref="ModelsProfileHelper.GetProfile"/>
|
||||
public static PowerDisplayProfile? GetProfile(string profileName) => ModelsProfileHelper.GetProfile(profileName);
|
||||
|
||||
// IProfileService explicit implementation
|
||||
|
||||
/// <inheritdoc/>
|
||||
PowerDisplayProfiles IProfileService.LoadProfiles() => ModelsProfileHelper.LoadProfiles();
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IProfileService.SaveProfiles(PowerDisplayProfiles profiles) => ModelsProfileHelper.SaveProfiles(profiles);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PowerDisplay.Common.Drivers;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for color temperature preset computation.
|
||||
/// Provides shared logic for computing available color presets from VCP capabilities.
|
||||
/// </summary>
|
||||
public static class ColorTemperatureHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes available color temperature presets from VCP value data.
|
||||
/// </summary>
|
||||
/// <param name="colorTemperatureValues">
|
||||
/// Collection of tuples containing (VcpValue, Name) for each color temperature preset.
|
||||
/// The VcpValue is the VCP value, Name is the name from capabilities string if available.
|
||||
/// </param>
|
||||
/// <returns>Sorted list of ColorPresetItem objects.</returns>
|
||||
public static List<ColorPresetItem> ComputeColorPresets(IEnumerable<(int VcpValue, string? Name)> colorTemperatureValues)
|
||||
{
|
||||
if (colorTemperatureValues == null)
|
||||
{
|
||||
return new List<ColorPresetItem>();
|
||||
}
|
||||
|
||||
var presetList = new List<ColorPresetItem>();
|
||||
|
||||
foreach (var item in colorTemperatureValues)
|
||||
{
|
||||
var displayName = FormatColorTemperatureDisplayName(item.VcpValue, item.Name);
|
||||
presetList.Add(new ColorPresetItem(item.VcpValue, displayName));
|
||||
}
|
||||
|
||||
// Sort by VCP value for consistent ordering
|
||||
return presetList.OrderBy(p => p.VcpValue).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a color temperature display name.
|
||||
/// Uses VcpNames for standard VCP value mappings if no custom name is provided.
|
||||
/// </summary>
|
||||
/// <param name="vcpValue">The VCP value.</param>
|
||||
/// <param name="customName">Optional custom name from capabilities string.</param>
|
||||
/// <returns>Formatted display name.</returns>
|
||||
public static string FormatColorTemperatureDisplayName(int vcpValue, string? customName = null)
|
||||
{
|
||||
// Priority: use name from VCP capabilities if available
|
||||
if (!string.IsNullOrEmpty(customName))
|
||||
{
|
||||
return customName;
|
||||
}
|
||||
|
||||
// Fall back to standard VCP value name from shared library
|
||||
return VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue)
|
||||
?? "Manufacturer Defined";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a display name for a custom (non-preset) color temperature value.
|
||||
/// Used when the current value is not in the available preset list.
|
||||
/// </summary>
|
||||
/// <param name="vcpValue">The VCP value.</param>
|
||||
/// <returns>Formatted display name with "Custom" indicator.</returns>
|
||||
public static string FormatCustomColorTemperatureDisplayName(int vcpValue)
|
||||
{
|
||||
var standardName = VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue);
|
||||
return string.IsNullOrEmpty(standardName)
|
||||
? "Custom"
|
||||
: $"{standardName} (Custom)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,58 +398,62 @@ namespace PowerDisplay.Common.Utils
|
||||
public bool TryParseEntry(out VcpEntry entry)
|
||||
{
|
||||
entry = default;
|
||||
SkipWhitespace();
|
||||
|
||||
if (IsAtEnd())
|
||||
while (true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SkipWhitespace();
|
||||
|
||||
// Parse hex byte (VCP code)
|
||||
if (!TryParseHexByte(out var code))
|
||||
{
|
||||
// Skip invalid character and try again
|
||||
_position++;
|
||||
return TryParseEntry(out entry);
|
||||
}
|
||||
|
||||
var values = new List<int>();
|
||||
|
||||
SkipWhitespace();
|
||||
|
||||
// Check for optional value list
|
||||
if (!IsAtEnd() && Peek() == '(')
|
||||
{
|
||||
_position++; // consume '('
|
||||
|
||||
// Parse values until ')'
|
||||
while (!IsAtEnd() && Peek() != ')')
|
||||
if (IsAtEnd())
|
||||
{
|
||||
SkipWhitespace();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Peek() == ')')
|
||||
// Parse hex byte (VCP code)
|
||||
if (!TryParseHexByte(out var code))
|
||||
{
|
||||
// Skip invalid character and try again
|
||||
_position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = new List<int>();
|
||||
|
||||
SkipWhitespace();
|
||||
|
||||
// Check for optional value list
|
||||
if (!IsAtEnd() && Peek() == '(')
|
||||
{
|
||||
_position++; // consume '('
|
||||
|
||||
// Parse values until ')'
|
||||
while (!IsAtEnd() && Peek() != ')')
|
||||
{
|
||||
break;
|
||||
SkipWhitespace();
|
||||
|
||||
if (Peek() == ')')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (TryParseHexByte(out var value))
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_position++; // Skip invalid character
|
||||
}
|
||||
}
|
||||
|
||||
if (TryParseHexByte(out var value))
|
||||
if (!IsAtEnd() && Peek() == ')')
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_position++; // Skip invalid character
|
||||
_position++; // consume ')'
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsAtEnd() && Peek() == ')')
|
||||
{
|
||||
_position++; // consume ')'
|
||||
}
|
||||
entry = new VcpEntry(code, values);
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = new VcpEntry(code, values);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseHexByte(out byte value)
|
||||
@@ -518,45 +522,48 @@ namespace PowerDisplay.Common.Utils
|
||||
code = 0;
|
||||
name = string.Empty;
|
||||
|
||||
SkipWhitespace();
|
||||
|
||||
if (IsAtEnd())
|
||||
while (true)
|
||||
{
|
||||
return false;
|
||||
SkipWhitespace();
|
||||
|
||||
if (IsAtEnd())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse hex byte
|
||||
if (!TryParseHexByte(out code))
|
||||
{
|
||||
_position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
|
||||
// Expect '('
|
||||
if (IsAtEnd() || Peek() != '(')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_position++; // consume '('
|
||||
|
||||
// Parse name until ')'
|
||||
int start = _position;
|
||||
while (!IsAtEnd() && Peek() != ')')
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
|
||||
name = _content.Slice(start, _position - start).ToString().Trim();
|
||||
|
||||
if (!IsAtEnd() && Peek() == ')')
|
||||
{
|
||||
_position++; // consume ')'
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse hex byte
|
||||
if (!TryParseHexByte(out code))
|
||||
{
|
||||
_position++;
|
||||
return TryParseEntry(out code, out name);
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
|
||||
// Expect '('
|
||||
if (IsAtEnd() || Peek() != '(')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_position++; // consume '('
|
||||
|
||||
// Parse name until ')'
|
||||
int start = _position;
|
||||
while (!IsAtEnd() && Peek() != ')')
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
|
||||
name = _content.Slice(start, _position - start).ToString().Trim();
|
||||
|
||||
if (!IsAtEnd() && Peek() == ')')
|
||||
{
|
||||
_position++; // consume ')'
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseHexByte(out byte value)
|
||||
|
||||
@@ -1,66 +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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for profile management.
|
||||
/// Provides shared logic for generating unique profile names and other profile-related operations.
|
||||
/// </summary>
|
||||
public static class ProfileHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Default base name for new profiles.
|
||||
/// </summary>
|
||||
public const string DefaultProfileBaseName = "Profile";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum counter value when generating unique profile names.
|
||||
/// </summary>
|
||||
private const int MaxProfileCounter = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique profile name that doesn't conflict with existing names.
|
||||
/// Uses the format "Profile N" where N is an incrementing number.
|
||||
/// </summary>
|
||||
/// <param name="existingNames">Set of existing profile names to avoid conflicts.</param>
|
||||
/// <param name="baseName">Optional base name to use (defaults to "Profile").</param>
|
||||
/// <returns>A unique profile name.</returns>
|
||||
public static string GenerateUniqueProfileName(ISet<string> existingNames, string? baseName = null)
|
||||
{
|
||||
if (existingNames == null)
|
||||
{
|
||||
existingNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var nameBase = string.IsNullOrEmpty(baseName) ? DefaultProfileBaseName : baseName;
|
||||
|
||||
// Start with base name without number
|
||||
if (!existingNames.Contains(nameBase))
|
||||
{
|
||||
return nameBase;
|
||||
}
|
||||
|
||||
// Try "Profile 2", "Profile 3", etc.
|
||||
int counter = 2;
|
||||
while (counter < MaxProfileCounter)
|
||||
{
|
||||
var candidateName = string.Format(CultureInfo.InvariantCulture, "{0} {1}", nameBase, counter);
|
||||
if (!existingNames.Contains(candidateName))
|
||||
{
|
||||
return candidateName;
|
||||
}
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Fallback with timestamp if somehow we hit the limit
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0} {1}", nameBase, DateTime.Now.Ticks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,6 @@ namespace PowerDisplay.Common.Utils
|
||||
/// </summary>
|
||||
public static class VcpNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the optional delegate to provide localized VCP code names.
|
||||
/// Set this at application startup to enable localization.
|
||||
/// The delegate receives a VCP code and should return the localized name, or null to use the default.
|
||||
/// </summary>
|
||||
public static Func<byte, string?>? LocalizedCodeNameProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// VCP code to name mapping
|
||||
/// </summary>
|
||||
|
||||
@@ -46,12 +46,15 @@ public static class NamedPipeProcessor
|
||||
{
|
||||
var message = await streamReader.ReadLineAsync(cancellationToken);
|
||||
|
||||
if (message != null)
|
||||
if (message == null)
|
||||
{
|
||||
Logger.LogInfo($"[NamedPipe] Received message: {message}");
|
||||
messageHandler(message);
|
||||
Logger.LogInfo("[NamedPipe] Pipe closed by server");
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.LogTrace($"[NamedPipe] Received message: {message}");
|
||||
messageHandler(message);
|
||||
|
||||
// Small delay to prevent tight loop
|
||||
var intraMessageDelay = TimeSpan.FromMilliseconds(10);
|
||||
await Task.Delay(intraMessageDelay, cancellationToken);
|
||||
|
||||
@@ -185,7 +185,12 @@ namespace PowerDisplay.Helpers
|
||||
private nint GetAppIconHandle()
|
||||
{
|
||||
var exePath = Path.Combine(AppContext.BaseDirectory, "PowerToys.PowerDisplay.exe");
|
||||
ExtractIconExNative(exePath, 0, out var largeIcon, out _, 1);
|
||||
ExtractIconExNative(exePath, 0, out var largeIcon, out var smallIcon, 1);
|
||||
if (smallIcon != 0)
|
||||
{
|
||||
DestroyIcon(smallIcon);
|
||||
}
|
||||
|
||||
return largeIcon;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace PowerDisplay.PowerDisplayXAML
|
||||
// Subclass WndProc to suppress WM_DPICHANGED during cross-DPI positioning
|
||||
_dpiSuppressor = new DpiSuppressor(this);
|
||||
|
||||
// Ensure DpiSuppressor is disposed when window closes
|
||||
this.Closed += (_, _) => Dispose();
|
||||
|
||||
// Auto close after 3 seconds
|
||||
Task.Delay(3000).ContinueWith(_ =>
|
||||
{
|
||||
|
||||
@@ -1,18 +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.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// IPC message wrapper for parsing action-based messages.
|
||||
/// Used in App.xaml.cs for dynamic IPC command handling.
|
||||
/// </summary>
|
||||
internal sealed class IpcMessageAction
|
||||
{
|
||||
[JsonPropertyName("action")]
|
||||
public string? Action { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace PowerDisplay.Serialization
|
||||
/// Note: MonitorStateFile and MonitorStateEntry are now in PowerDisplay.Lib
|
||||
/// and should be serialized using ProfileSerializationContext from the Lib.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(IpcMessageAction))]
|
||||
[JsonSerializable(typeof(PowerDisplaySettings))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfiles))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfile))]
|
||||
|
||||
@@ -125,6 +125,12 @@ public partial class MainViewModel
|
||||
|
||||
private void UpdateMonitorList(IReadOnlyList<Monitor> monitors, bool isInitialLoad)
|
||||
{
|
||||
// Dispose old ViewModels to unsubscribe PropertyChanged handlers
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
vm.Dispose();
|
||||
}
|
||||
|
||||
Monitors.Clear();
|
||||
|
||||
// Load settings to check for hidden monitors
|
||||
|
||||
@@ -50,7 +50,7 @@ public partial class MainViewModel
|
||||
|
||||
// Apply UI configuration changes only (feature visibility toggles, etc.)
|
||||
// Hardware parameters (brightness, color temperature) are applied via custom actions
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
|
||||
ApplyUIConfiguration(settings);
|
||||
|
||||
// Reload profiles in case they were added/updated/deleted in Settings UI
|
||||
@@ -233,7 +233,8 @@ public partial class MainViewModel
|
||||
}
|
||||
|
||||
// Apply color temperature if included in profile
|
||||
if (setting.ColorTemperatureVcp.HasValue && setting.ColorTemperatureVcp.Value > 0)
|
||||
if (setting.ColorTemperatureVcp.HasValue && setting.ColorTemperatureVcp.Value > 0 &&
|
||||
monitorVm.ShowColorTemperature)
|
||||
{
|
||||
updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperatureVcp.Value));
|
||||
}
|
||||
@@ -273,7 +274,8 @@ public partial class MainViewModel
|
||||
}
|
||||
|
||||
// Restore color temperature if different from current
|
||||
if (savedState.Value.ColorTemperatureVcp > 0 &&
|
||||
if (monitorVm.ShowColorTemperature &&
|
||||
savedState.Value.ColorTemperatureVcp > 0 &&
|
||||
savedState.Value.ColorTemperatureVcp != monitorVm.ColorTemperature)
|
||||
{
|
||||
updateTasks.Add(monitorVm.SetColorTemperatureAsync(savedState.Value.ColorTemperatureVcp));
|
||||
|
||||
@@ -59,36 +59,6 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
|
||||
private bool _showPowerState;
|
||||
|
||||
/// <summary>
|
||||
/// Updates a property value directly without triggering hardware updates.
|
||||
/// Used during initialization to update UI from saved state.
|
||||
/// </summary>
|
||||
internal void UpdatePropertySilently(string propertyName, int value)
|
||||
{
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Brightness):
|
||||
_brightness = value;
|
||||
OnPropertyChanged(nameof(Brightness));
|
||||
break;
|
||||
case nameof(Contrast):
|
||||
_contrast = value;
|
||||
OnPropertyChanged(nameof(Contrast));
|
||||
OnPropertyChanged(nameof(ContrastPercent));
|
||||
break;
|
||||
case nameof(Volume):
|
||||
_volume = value;
|
||||
OnPropertyChanged(nameof(Volume));
|
||||
break;
|
||||
case nameof(ColorTemperature):
|
||||
// Update underlying monitor model
|
||||
_monitor.CurrentColorTemperature = value;
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply brightness with hardware update and state persistence.
|
||||
/// </summary>
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Constants.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PowerDisplayProcessManager.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Constants.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Trace.h">
|
||||
<ClInclude Include="Trace.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
|
||||
@@ -74,31 +74,6 @@ void PowerDisplayProcessManager::send_message(const std::wstring& message_type,
|
||||
});
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::bring_to_front()
|
||||
{
|
||||
submit_task([this] {
|
||||
if (!is_process_running())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
const auto process_handle = reinterpret_cast<HANDLE>(param);
|
||||
DWORD window_process_id = 0;
|
||||
|
||||
GetWindowThreadProcessId(hwnd, &window_process_id);
|
||||
if (GetProcessId(process_handle) == window_process_id)
|
||||
{
|
||||
SetForegroundWindow(hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
};
|
||||
|
||||
EnumWindows(enum_windows, reinterpret_cast<LPARAM>(m_hProcess));
|
||||
});
|
||||
}
|
||||
|
||||
bool PowerDisplayProcessManager::is_running() const
|
||||
{
|
||||
return is_process_running();
|
||||
|
||||
@@ -39,11 +39,6 @@ public:
|
||||
/// <param name="message_arg">Optional message argument</param>
|
||||
void send_message(const std::wstring& message_type, const std::wstring& message_arg = L"");
|
||||
|
||||
/// <summary>
|
||||
/// Bring the PowerDisplay window to the foreground.
|
||||
/// </summary>
|
||||
void bring_to_front();
|
||||
|
||||
/// <summary>
|
||||
/// Check if PowerDisplay.exe process is running.
|
||||
/// </summary>
|
||||
|
||||
@@ -39,7 +39,7 @@ const static wchar_t* MODULE_DESC = L"A utility to manage display brightness and
|
||||
class PowerDisplayModule : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
std::atomic<bool> m_enabled = false;
|
||||
|
||||
// Process manager handles Named Pipe communication and process lifecycle
|
||||
PowerDisplayProcessManager m_processManager;
|
||||
@@ -81,10 +81,11 @@ public:
|
||||
|
||||
if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent || !m_hToggleEvent || !m_hStopEvent)
|
||||
{
|
||||
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}, Toggle={}",
|
||||
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}, Toggle={}, Stop={}",
|
||||
reinterpret_cast<void*>(m_hRefreshEvent),
|
||||
reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent),
|
||||
reinterpret_cast<void*>(m_hToggleEvent));
|
||||
reinterpret_cast<void*>(m_hToggleEvent),
|
||||
reinterpret_cast<void*>(m_hStopEvent));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -100,9 +101,6 @@ public:
|
||||
disable();
|
||||
}
|
||||
|
||||
// Stop toggle event listener thread
|
||||
StopToggleEventListener();
|
||||
|
||||
// Clean up event handles
|
||||
if (m_hRefreshEvent)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <strsafe.h>
|
||||
#include <hIdUsage.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||