mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-06 10:30:29 +02:00
Compare commits
15 Commits
feature/ru
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffddb0a1b5 | ||
|
|
4bc489feb8 | ||
|
|
96000a10ed | ||
|
|
e6d346a59b | ||
|
|
2aece74831 | ||
|
|
7861bc408c | ||
|
|
b835cde4d2 | ||
|
|
f79df0663c | ||
|
|
7211f7ed67 | ||
|
|
215dfaf236 | ||
|
|
f5a294bb66 | ||
|
|
d10203b8ac | ||
|
|
fecd2e72a7 | ||
|
|
fde1599f7d | ||
|
|
c68003c678 |
9
.github/actions/spell-check/allow/code.txt
vendored
9
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,7 +330,9 @@ MRUINFO
|
||||
REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
DEFAULTTONEAREST
|
||||
INVOKEIDLIST
|
||||
LCMAP
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
Mdt
|
||||
@@ -394,3 +396,10 @@ Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
# Program names
|
||||
SEARCHHOST
|
||||
SHELLEXPERIENCEHOST
|
||||
SHELLHOST
|
||||
STARTMENUEXPERIENCEHOST
|
||||
WIDGETBOARD
|
||||
|
||||
3
.github/actions/spell-check/expect.txt
vendored
3
.github/actions/spell-check/expect.txt
vendored
@@ -365,6 +365,7 @@ defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
defaulttonearest
|
||||
DEFAULTTONEAREST
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -1367,6 +1368,7 @@ POINTERUPDATE
|
||||
Pokedex
|
||||
Pomodoro
|
||||
Popups
|
||||
popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
@@ -1882,6 +1884,7 @@ toolwindow
|
||||
TOPDOWNDIB
|
||||
TOUCHEVENTF
|
||||
TOUCHINPUT
|
||||
touchpads
|
||||
TPMLEFTALIGN
|
||||
TPMRETURNCMD
|
||||
TRACEHANDLE
|
||||
|
||||
22
README.md
22
README.md
@@ -50,18 +50,18 @@ But to get started quickly, choose one of the installation methods below:
|
||||
Go to the [PowerToys GitHub releases](https://aka.ms/installPowerToys), select **Assets** to reveal the installation files, and choose the one that matches your architecture and install scope. For most devices, that would be _x64 per-user_.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.100%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysUserSetup-0.99.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysUserSetup-0.99.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysSetup-0.99.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysSetup-0.99.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
| --- | --- |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.98.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.99.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.99.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.99.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.99.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -106,11 +106,11 @@ There are [community driven install methods](https://learn.microsoft.com/windows
|
||||
|
||||
[](https://github.com/microsoft/PowerToys/releases)
|
||||
|
||||
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
|
||||
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.99.0).
|
||||
|
||||
## 🛣️ Roadmap
|
||||
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – a brand-new Shortcut Guide experience, ensuring it's easier to find and install Command Palette extensions and so much more! Stay tuned for [v0.100][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 258 KiB |
@@ -579,14 +579,16 @@ static void StopResizing()
|
||||
HideOverlay();
|
||||
}
|
||||
|
||||
static void ReplayAbsorbedAlt()
|
||||
static void ReplayAbsorbedModifier(bool alsoKeyUp)
|
||||
{
|
||||
INPUT keyboardInput = {};
|
||||
keyboardInput.type = INPUT_KEYBOARD;
|
||||
keyboardInput.ki.wVk = static_cast<WORD>(g_absorbedVk);
|
||||
keyboardInput.ki.wScan = static_cast<WORD>(g_absorbedScanCode);
|
||||
keyboardInput.ki.dwFlags = (g_absorbedFlags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0;
|
||||
SendInput(1, &keyboardInput, sizeof(INPUT));
|
||||
INPUT inputs[2] = {};
|
||||
inputs[0].type = INPUT_KEYBOARD;
|
||||
inputs[0].ki.wVk = static_cast<WORD>(g_absorbedVk);
|
||||
inputs[0].ki.wScan = static_cast<WORD>(g_absorbedScanCode);
|
||||
inputs[0].ki.dwFlags = (g_absorbedFlags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0;
|
||||
inputs[1] = inputs[0];
|
||||
inputs[1].ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
SendInput(alsoKeyUp ? 2 : 1, inputs, sizeof(INPUT));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -632,9 +634,65 @@ static void ShowTrayMenu(HWND hwnd)
|
||||
|
||||
static bool IsSystemClass(HWND hwnd)
|
||||
{
|
||||
wchar_t cls[64] = {};
|
||||
GetClassNameW(hwnd, cls, 64);
|
||||
return (wcscmp(cls, L"Progman") == 0 || wcscmp(cls, L"Shell_TrayWnd") == 0);
|
||||
wchar_t cls[256] = {};
|
||||
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
|
||||
|
||||
// Desktop and primary/secondary taskbars
|
||||
if (wcscmp(cls, L"Progman") == 0 ||
|
||||
wcscmp(cls, L"Shell_TrayWnd") == 0 ||
|
||||
wcscmp(cls, L"Shell_SecondaryTrayWnd") == 0)
|
||||
return true;
|
||||
|
||||
// System tray / notification area popups and overflow
|
||||
if (wcscmp(cls, L"NotifyIconOverflowWindow") == 0 ||
|
||||
wcscmp(cls, L"TopLevelWindowForOverflowXamlIsland") == 0)
|
||||
return true;
|
||||
|
||||
// Tooltips (e.g. "Show hidden icons" tooltip)
|
||||
if (wcscmp(cls, L"tooltips_class32") == 0)
|
||||
return true;
|
||||
|
||||
// Task View (Win+Tab)
|
||||
if (wcscmp(cls, L"MultitaskingViewFrame") == 0 ||
|
||||
wcscmp(cls, L"XamlExplorerHostIslandWindow") == 0)
|
||||
return true;
|
||||
|
||||
// System tray flyouts (Quick Settings, calendar, input switcher)
|
||||
if (wcscmp(cls, L"Windows.UI.Composition.DesktopWindowContentBridge") == 0 ||
|
||||
wcscmp(cls, L"Shell_InputSwitchTopLevelWindow") == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::wstring ToUpperInvariant(std::wstring_view input)
|
||||
{
|
||||
int required = LCMapStringEx(
|
||||
LOCALE_NAME_INVARIANT,
|
||||
LCMAP_UPPERCASE,
|
||||
input.data(),
|
||||
static_cast<int>(input.size()),
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0);
|
||||
|
||||
std::wstring result(required, L'\0');
|
||||
|
||||
LCMapStringEx(
|
||||
LOCALE_NAME_INVARIANT,
|
||||
LCMAP_UPPERCASE,
|
||||
input.data(),
|
||||
static_cast<int>(input.size()),
|
||||
result.data(),
|
||||
required,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool IsExcluded(HWND hwnd)
|
||||
@@ -642,6 +700,45 @@ static bool IsExcluded(HWND hwnd)
|
||||
if (IsSystemClass(hwnd))
|
||||
return true;
|
||||
|
||||
// To identify these for adding a new exception:
|
||||
// 1. Resolve the hwnd class name.
|
||||
// 2. Resolve the process path.
|
||||
// 3. Add OutputDebugStringW() for the class name and process path.
|
||||
// 4. Build the executable.
|
||||
// 5. Check with the debugger (or with Sysinternals DebugView) the outputs.
|
||||
// 6. Delete the added code.
|
||||
// 7. Add the exception below, according to the pattern there.
|
||||
//
|
||||
// Shell experience windows: Start menu, Notifications (Win+N), Search,
|
||||
// Quick Settings (volume / network / battery).
|
||||
// These use the generic Windows.UI.Core.CoreWindow class, so filter by process.
|
||||
{
|
||||
wchar_t cls[256] = {};
|
||||
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
|
||||
if (wcscmp(cls, L"Windows.UI.Core.CoreWindow") == 0)
|
||||
{
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"STARTMENUEXPERIENCEHOST.EXE") != std::wstring::npos ||
|
||||
processPath.find(L"SHELLEXPERIENCEHOST.EXE") != std::wstring::npos ||
|
||||
processPath.find(L"SEARCHHOST.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
else if (wcscmp(cls, L"ControlCenterWindow") == 0)
|
||||
{
|
||||
// The Quick Settings flyout.
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"SHELLHOST.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
else if (wcscmp(cls, L"WindowsDashboard") == 0)
|
||||
{
|
||||
// The Windows 11 Widgets flyout.
|
||||
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
|
||||
if (processPath.find(L"WIDGETBOARD.EXE") != std::wstring::npos)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
auto apps = g_excludedApps.load();
|
||||
if (!apps || apps->empty())
|
||||
return false;
|
||||
@@ -735,8 +832,9 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
g_dragConsumedAlt = false;
|
||||
return 1;
|
||||
}
|
||||
// No drag happened; replay the keydown, then let keyup through
|
||||
ReplayAbsorbedAlt();
|
||||
// No drag happened; replay the keydown, THEN the keyup
|
||||
ReplayAbsorbedModifier(true);
|
||||
return 1; // swallow this keyup since the replay already sent one
|
||||
}
|
||||
}
|
||||
goto forward; // let Win keyup pass through
|
||||
@@ -793,7 +891,7 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
return 1;
|
||||
}
|
||||
// No drag happened; replay the keydown, then let keyup through
|
||||
ReplayAbsorbedAlt();
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,7 +901,8 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
if (g_altAbsorbed && !g_dragConsumedAlt)
|
||||
{
|
||||
g_altAbsorbed = false;
|
||||
ReplayAbsorbedAlt();
|
||||
g_altPressed = false;
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -813,7 +912,7 @@ static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
g_winAbsorbed = false;
|
||||
g_winPressed = false;
|
||||
ReplayAbsorbedAlt();
|
||||
ReplayAbsorbedModifier(false);
|
||||
}
|
||||
|
||||
// Track held non-modifier keys (used to suppress GrabAndMove when the modifier
|
||||
|
||||
@@ -9784,7 +9784,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, ZOOM_HOTKEY, g_ToggleMod, g_ToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9793,7 +9796,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
if (!RegisterHotKey(hWnd, LIVE_HOTKEY, g_LiveZoomToggleMod, g_LiveZoomToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, LIVE_DRAW_HOTKEY, g_LiveZoomToggleMod ^ MOD_SHIFT, g_LiveZoomToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified live-zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified live-zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9801,7 +9807,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, DRAW_HOTKEY, g_DrawToggleMod, g_DrawToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified draw w/out zoom hotkey is already in use.\nSelect a different draw w/out zoom hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified draw w/out zoom hotkey is already in use.\nSelect a different draw w/out zoom hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9809,7 +9818,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, BREAK_HOTKEY, g_BreakToggleMod, g_BreakToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified break timer hotkey is already in use.\nSelect a different break timer hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified break timer hotkey is already in use.\nSelect a different break timer hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9818,7 +9830,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
if (!RegisterHotKey(hWnd, DEMOTYPE_HOTKEY, g_DemoTypeToggleMod, g_DemoTypeToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, DEMOTYPE_RESET_HOTKEY, (g_DemoTypeToggleMod ^ MOD_SHIFT), g_DemoTypeToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified live-type hotkey is already in use.\nSelect a different live-type hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified live-type hotkey is already in use.\nSelect a different live-type hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9827,7 +9842,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9837,7 +9855,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9845,7 +9866,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
{
|
||||
if (!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip OCR hotkey is already in use.\nSelect a different snip OCR hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified snip OCR hotkey is already in use.\nSelect a different snip OCR hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
@@ -9855,7 +9879,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
!RegisterHotKey(hWnd, RECORD_CROP_HOTKEY, (g_RecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF) ||
|
||||
!RegisterHotKey(hWnd, RECORD_WINDOW_HOTKEY, (g_RecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF))
|
||||
{
|
||||
MessageBox(hWnd, L"The specified record hotkey is already in use.\nSelect a different record hotkey.", APPNAME, MB_ICONERROR);
|
||||
if(!g_StartedByPowerToys)
|
||||
{
|
||||
MessageBox(hWnd, L"The specified record hotkey is already in use.\nSelect a different record hotkey.", APPNAME, MB_ICONERROR);
|
||||
}
|
||||
showOptions = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -11,9 +12,21 @@ public record AppStateModel
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// STATE HERE
|
||||
// Make sure that any new types you add are added to JsonSerializationContext!
|
||||
public RecentCommandsManager RecentCommands { get; init; } = new();
|
||||
private RecentCommandsManager? _recentCommands = new();
|
||||
|
||||
public ImmutableList<string> RunHistory { get; init; } = ImmutableList<string>.Empty;
|
||||
public RecentCommandsManager RecentCommands
|
||||
{
|
||||
get => _recentCommands ?? new();
|
||||
init => _recentCommands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<string>? _runHistory = ImmutableList<string>.Empty;
|
||||
|
||||
public ImmutableList<string> RunHistory
|
||||
{
|
||||
get => _runHistory ?? ImmutableList<string>.Empty;
|
||||
init => _runHistory = value;
|
||||
}
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -46,45 +46,6 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
var newShowTitles = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => (bool?)null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
UpdateModel(_dockSettingsModel with { ShowTitles = newShowTitles });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
@@ -138,7 +99,6 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsService = settingsService;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
|
||||
@@ -559,7 +559,7 @@ public sealed partial class DockViewModel
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId };
|
||||
var dockSettings = _settings;
|
||||
|
||||
// Create the band view model
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -18,12 +18,24 @@ public record ProviderSettings
|
||||
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
|
||||
public ImmutableDictionary<string, FallbackSettings> FallbackCommands { get; init; }
|
||||
private ImmutableDictionary<string, FallbackSettings>? _fallbackCommands
|
||||
= ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||
|
||||
public ImmutableList<string> PinnedCommandIds { get; init; }
|
||||
public ImmutableDictionary<string, FallbackSettings> FallbackCommands
|
||||
{
|
||||
get => _fallbackCommands ?? ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||
init => _fallbackCommands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<string>? _pinnedCommandIds
|
||||
= ImmutableList<string>.Empty;
|
||||
|
||||
public ImmutableList<string> PinnedCommandIds
|
||||
{
|
||||
get => _pinnedCommandIds ?? ImmutableList<string>.Empty;
|
||||
init => _pinnedCommandIds = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string ProviderId { get; init; } = string.Empty;
|
||||
|
||||
@@ -37,7 +49,6 @@ public record ProviderSettings
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public ProviderSettings(bool isEnabled)
|
||||
{
|
||||
IsEnabled = isEnabled;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public record RecentCommandsManager : IRecentCommandsManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal ImmutableList<HistoryItem> History { get; init; } = ImmutableList<HistoryItem>.Empty;
|
||||
private ImmutableList<HistoryItem>? _history = ImmutableList<HistoryItem>.Empty;
|
||||
|
||||
internal ImmutableList<HistoryItem> History
|
||||
{
|
||||
get => _history ?? ImmutableList<HistoryItem>.Empty;
|
||||
init => _history = value;
|
||||
}
|
||||
|
||||
public RecentCommandsManager()
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Windows.UI;
|
||||
|
||||
@@ -44,7 +45,7 @@ public record DockSettings
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
|
||||
private ImmutableList<DockBandSettings>? _startBands = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
||||
@@ -54,12 +55,24 @@ public record DockSettings
|
||||
{
|
||||
ProviderId = "WinGet",
|
||||
CommandId = "com.microsoft.cmdpal.winget",
|
||||
ShowLabels = false,
|
||||
ShowTitles = false,
|
||||
});
|
||||
|
||||
public ImmutableList<DockBandSettings> CenterBands { get; init; } = ImmutableList<DockBandSettings>.Empty;
|
||||
public ImmutableList<DockBandSettings> StartBands
|
||||
{
|
||||
get => _startBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _startBands = value;
|
||||
}
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands { get; init; } = ImmutableList.Create(
|
||||
private ImmutableList<DockBandSettings>? _centerBands = ImmutableList<DockBandSettings>.Empty;
|
||||
|
||||
public ImmutableList<DockBandSettings> CenterBands
|
||||
{
|
||||
get => _centerBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _centerBands = value;
|
||||
}
|
||||
|
||||
private ImmutableList<DockBandSettings>? _endBands = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "PerformanceMonitor",
|
||||
@@ -71,6 +84,12 @@ public record DockSettings
|
||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
});
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands
|
||||
{
|
||||
get => _endBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _endBands = value;
|
||||
}
|
||||
|
||||
public bool ShowLabels { get; init; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
@@ -102,16 +121,6 @@ public record DockBandSettings
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
init => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
@@ -135,12 +144,52 @@ public enum DockSide
|
||||
Bottom = 3,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(DockSizeJsonConverter))]
|
||||
public enum DockSize
|
||||
{
|
||||
Default,
|
||||
Compact,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom converter for <see cref="DockSize"/> that preserves backward
|
||||
/// compatibility with previously-persisted values. Earlier builds shipped a
|
||||
/// <c>Small</c>/<c>Medium</c>/<c>Large</c> enum; those values are migrated to
|
||||
/// <see cref="DockSize.Default"/> so existing settings.json files continue to
|
||||
/// load instead of failing the entire deserialization.
|
||||
/// </summary>
|
||||
internal sealed class DockSizeJsonConverter : JsonConverter<DockSize>
|
||||
{
|
||||
public override DockSize Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (Enum.TryParse<DockSize>(value, ignoreCase: true, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Legacy values from the original Small/Medium/Large enum, or any
|
||||
// other unknown string — fall back to Default so the user's
|
||||
// settings file remains loadable after upgrading.
|
||||
return DockSize.Default;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var number))
|
||||
{
|
||||
return Enum.IsDefined(typeof(DockSize), number) ? (DockSize)number : DockSize.Default;
|
||||
}
|
||||
|
||||
return DockSize.Default;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DockSize value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
{
|
||||
Transparent,
|
||||
|
||||
@@ -55,8 +55,14 @@ public record HotkeySettings// : ICmdLineRepresentable
|
||||
|
||||
// This is currently needed for FancyZones, we need to unify these two objects
|
||||
// see src\common\settings_objects.h
|
||||
private string? _key = string.Empty;
|
||||
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Key
|
||||
{
|
||||
get => _key ?? string.Empty;
|
||||
init => _key = value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -39,17 +39,41 @@ public record SettingsModel
|
||||
|
||||
public bool AllowExternalReload { get; init; }
|
||||
|
||||
public ImmutableDictionary<string, ProviderSettings> ProviderSettings { get; init; }
|
||||
private ImmutableDictionary<string, ProviderSettings>? _providerSettings
|
||||
= ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
|
||||
public string[] FallbackRanks { get; init; } = [];
|
||||
public ImmutableDictionary<string, ProviderSettings> ProviderSettings
|
||||
{
|
||||
get => _providerSettings ?? ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
init => _providerSettings = value;
|
||||
}
|
||||
|
||||
public ImmutableDictionary<string, CommandAlias> Aliases { get; init; }
|
||||
private string[]? _fallbackRanks = [];
|
||||
|
||||
public string[] FallbackRanks
|
||||
{
|
||||
get => _fallbackRanks ?? [];
|
||||
init => _fallbackRanks = value;
|
||||
}
|
||||
|
||||
private ImmutableDictionary<string, CommandAlias>? _aliases
|
||||
= ImmutableDictionary<string, CommandAlias>.Empty;
|
||||
|
||||
public ImmutableList<TopLevelHotkey> CommandHotkeys { get; init; }
|
||||
public ImmutableDictionary<string, CommandAlias> Aliases
|
||||
{
|
||||
get => _aliases ?? ImmutableDictionary<string, CommandAlias>.Empty;
|
||||
init => _aliases = value;
|
||||
}
|
||||
|
||||
private ImmutableList<TopLevelHotkey>? _commandHotkeys
|
||||
= ImmutableList<TopLevelHotkey>.Empty;
|
||||
|
||||
public ImmutableList<TopLevelHotkey> CommandHotkeys
|
||||
{
|
||||
get => _commandHotkeys ?? ImmutableList<TopLevelHotkey>.Empty;
|
||||
init => _commandHotkeys = value;
|
||||
}
|
||||
|
||||
public MonitorBehavior SummonOn { get; init; } = MonitorBehavior.ToMouse;
|
||||
|
||||
public bool DisableAnimations { get; init; } = true;
|
||||
@@ -62,7 +86,13 @@ public record SettingsModel
|
||||
|
||||
public bool EnableDock { get; init; }
|
||||
|
||||
public DockSettings DockSettings { get; init; } = new();
|
||||
private DockSettings? _dockSettings = new();
|
||||
|
||||
public DockSettings DockSettings
|
||||
{
|
||||
get => _dockSettings ?? new();
|
||||
init => _dockSettings = value;
|
||||
}
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||
|
||||
@@ -196,7 +196,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
ProviderId = this.CommandProviderId,
|
||||
CommandId = this.Id,
|
||||
ShowLabels = true,
|
||||
ShowTitles = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Windows.Win32.Foundation;
|
||||
using static PowerDisplay.Common.Drivers.NativeConstants;
|
||||
using static PowerDisplay.Common.Drivers.PInvoke;
|
||||
@@ -27,32 +28,55 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogWarning("DDC: Monitor ignored - null physical monitor handle");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
var handleHex = $"0x{hPhysicalMonitor:X}";
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get capabilities string (slow I2C operation)
|
||||
var capsString = TryGetCapabilitiesString(hPhysicalMonitor);
|
||||
if (string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - empty capabilities string from DDC/CI");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"DDC: Capabilities raw (handle={handleHex}, length={capsString.Length}): {capsString}");
|
||||
|
||||
// Parse the capabilities string
|
||||
var parseResult = Utils.MccsCapabilitiesParser.Parse(capsString);
|
||||
var capabilities = parseResult.Capabilities;
|
||||
|
||||
if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - parsed capabilities have no VCP codes (parseErrors={parseResult.Errors.Count})");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
|
||||
// Check if brightness (VCP 0x10) is supported - determines DDC/CI validity
|
||||
bool supportsBrightness = capabilities.SupportsVcpCode(NativeConstants.VcpCodeBrightness);
|
||||
bool supportsContrast = capabilities.SupportsVcpCode(NativeConstants.VcpCodeContrast);
|
||||
bool supportsColorTemperature = capabilities.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset);
|
||||
bool supportsVolume = capabilities.SupportsVcpCode(NativeConstants.VcpCodeVolume);
|
||||
|
||||
Logger.LogInfo(
|
||||
$"DDC: Capabilities parsed (handle={handleHex}) - " +
|
||||
$"Brightness={supportsBrightness} Contrast={supportsContrast} " +
|
||||
$"ColorTemperature={supportsColorTemperature} Volume={supportsVolume}");
|
||||
|
||||
if (!supportsBrightness)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Monitor ignored (handle={handleHex}) - brightness (VCP 0x10) not advertised in capabilities");
|
||||
}
|
||||
|
||||
return new DdcCiValidationResult(supportsBrightness, capsString, capabilities);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException)
|
||||
{
|
||||
Logger.LogError($"DDC: Monitor ignored (handle={handleHex}) - exception during FetchCapabilities: {ex.Message}");
|
||||
return DdcCiValidationResult.Invalid;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +98,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
// Get capabilities string length
|
||||
if (!GetCapabilitiesStringLength(hPhysicalMonitor, out uint length) || length == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetCapabilitiesStringLength failed (handle=0x{hPhysicalMonitor:X}, length={length})");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,6 +108,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
if (!CapabilitiesRequestAndCapabilitiesReply(hPhysicalMonitor, buffer, length))
|
||||
{
|
||||
Logger.LogWarning($"DDC: CapabilitiesRequestAndCapabilitiesReply failed (handle=0x{hPhysicalMonitor:X})");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,6 +121,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException)
|
||||
{
|
||||
Logger.LogError($"DDC: TryGetCapabilitiesString exception (handle=0x{hPhysicalMonitor:X}): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Common.Interfaces;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
using WmiLight;
|
||||
using Monitor = PowerDisplay.Common.Models.Monitor;
|
||||
|
||||
@@ -245,8 +244,9 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
|
||||
/// <summary>
|
||||
/// Discover supported monitors.
|
||||
/// WMI brightness control is typically only available on internal laptop displays,
|
||||
/// which don't have meaningful UserFriendlyName in WmiMonitorID, so we use "Built-in Display".
|
||||
/// WMI brightness control is typically only available on internal laptop displays.
|
||||
/// The monitor Name is left blank here; the ViewModel layer fills in a localized
|
||||
/// "Built-in Display" string so it can be translated for the user's UI language.
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -294,13 +294,12 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
? $"WMI_{edidId}_{monitorNumber}"
|
||||
: $"WMI_Unknown_{monitorNumber}";
|
||||
|
||||
// Get display name from PnP manufacturer ID (e.g., "Lenovo Built-in Display")
|
||||
var displayName = PnpIdHelper.GetBuiltInDisplayName(edidId);
|
||||
|
||||
// Name is left blank: MonitorViewModel injects a localized
|
||||
// "Built-in Display" string for internal displays.
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = uniqueId,
|
||||
Name = displayName,
|
||||
Name = string.Empty,
|
||||
CurrentBrightness = currentBrightness,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
|
||||
@@ -1,86 +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.Frozen;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Common.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for mapping PnP (Plug and Play) manufacturer IDs to display names.
|
||||
/// PnP IDs are 3-character codes assigned by Microsoft to hardware manufacturers.
|
||||
/// See: https://uefi.org/pnp_id_list
|
||||
/// </summary>
|
||||
public static class PnpIdHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Map of common laptop/monitor manufacturer PnP IDs to display names.
|
||||
/// Only includes manufacturers known to produce laptops with internal displays.
|
||||
/// </summary>
|
||||
private static readonly FrozenDictionary<string, string> ManufacturerNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Major laptop manufacturers
|
||||
{ "ACR", "Acer" },
|
||||
{ "AUO", "AU Optronics" },
|
||||
{ "BOE", "BOE" },
|
||||
{ "CMN", "Chi Mei Innolux" },
|
||||
{ "DEL", "Dell" },
|
||||
{ "HWP", "HP" },
|
||||
{ "IVO", "InfoVision" },
|
||||
{ "LEN", "Lenovo" },
|
||||
{ "LGD", "LG Display" },
|
||||
{ "NCP", "Nanjing CEC Panda" },
|
||||
{ "SAM", "Samsung" },
|
||||
{ "SDC", "Samsung Display" },
|
||||
{ "SEC", "Samsung Electronics" },
|
||||
{ "SHP", "Sharp" },
|
||||
{ "AUS", "ASUS" },
|
||||
{ "MSI", "MSI" },
|
||||
{ "APP", "Apple" },
|
||||
{ "SNY", "Sony" },
|
||||
{ "PHL", "Philips" },
|
||||
{ "HSD", "HannStar" },
|
||||
{ "CPT", "Chunghwa Picture Tubes" },
|
||||
{ "QDS", "Quanta Display" },
|
||||
{ "TMX", "Tianma Microelectronics" },
|
||||
{ "CSO", "CSOT" },
|
||||
|
||||
// Microsoft Surface
|
||||
{ "MSF", "Microsoft" },
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Extract the 3-character PnP manufacturer ID from an EDID ID.
|
||||
/// </summary>
|
||||
/// <param name="edidId">EDID ID like "LEN4038" or "BOE0900".</param>
|
||||
/// <returns>The 3-character PnP ID (e.g., "LEN"), or null if invalid.</returns>
|
||||
public static string? ExtractPnpId(string? edidId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(edidId) || edidId.Length < 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// PnP ID is the first 3 characters
|
||||
return edidId.Substring(0, 3).ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a user-friendly display name for an internal display based on its EDID ID.
|
||||
/// </summary>
|
||||
/// <param name="edidId">EDID ID like "LEN4038" or "BOE0900".</param>
|
||||
/// <returns>Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback.</returns>
|
||||
public static string GetBuiltInDisplayName(string? edidId)
|
||||
{
|
||||
var pnpId = ExtractPnpId(edidId);
|
||||
|
||||
if (pnpId != null && ManufacturerNames.TryGetValue(pnpId, out var manufacturer))
|
||||
{
|
||||
return $"{manufacturer} Built-in Display";
|
||||
}
|
||||
|
||||
return "Built-in Display";
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,10 @@
|
||||
<data name="BuiltInMonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Built-in display</value>
|
||||
</data>
|
||||
<data name="BuiltInDisplayName" xml:space="preserve">
|
||||
<value>Built-in display</value>
|
||||
<comment>Display name shown in the monitor list for the laptop's internal/built-in display.</comment>
|
||||
</data>
|
||||
<data name="BrightnessTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Brightness</value>
|
||||
</data>
|
||||
|
||||
@@ -415,13 +415,17 @@ public partial class MainViewModel
|
||||
SupportsVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
|
||||
SupportsPowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
|
||||
|
||||
// Default Enable* to match Supports* for new monitors (first-time setup)
|
||||
// ApplyPreservedUserSettings will override these with saved user preferences if they exist
|
||||
// Default Enable* for new monitors (first-time setup):
|
||||
// - Contrast / Volume: enabled if the monitor advertises the VCP code (low-risk features).
|
||||
// - InputSource / ColorTemperature / PowerState: always disabled by default. These can leave
|
||||
// the monitor in a state recoverable only via physical buttons; users opt-in via the
|
||||
// Settings UI checkbox, which raises a confirmation dialog (HandleDangerousFeatureClickAsync).
|
||||
// ApplyPreservedUserSettings will override these with saved user preferences if they exist.
|
||||
EnableContrast = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x12) ?? false,
|
||||
EnableVolume = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x62) ?? false,
|
||||
EnableInputSource = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x60) ?? false,
|
||||
EnableColorTemperature = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0x14) ?? false,
|
||||
EnablePowerState = vm.VcpCapabilitiesInfo?.SupportedVcpCodes.ContainsKey(0xD6) ?? false,
|
||||
EnableInputSource = false,
|
||||
EnableColorTemperature = false,
|
||||
EnablePowerState = false,
|
||||
|
||||
// Monitor number for display name formatting
|
||||
MonitorNumber = vm.MonitorNumber,
|
||||
|
||||
@@ -215,13 +215,19 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
// Subscribe to underlying Monitor property changes (e.g., Orientation updates in mirror mode)
|
||||
_monitor.PropertyChanged += OnMonitorPropertyChanged;
|
||||
|
||||
// Initialize Show properties based on hardware capabilities
|
||||
// Initialize Show properties for first-time detection. ApplyFeatureVisibility will
|
||||
// override these whenever settings.json has a saved entry for this monitor, so these
|
||||
// values only take effect for brand-new monitors (no persisted preference yet).
|
||||
// Mirror CreateMonitorInfo's defaults to keep the flyout and settings.json in sync:
|
||||
// - Brightness / Contrast / Volume: enabled if the hardware advertises the VCP code.
|
||||
// - InputSource / ColorTemperature / PowerState: always disabled by default (dangerous
|
||||
// features); the user opts in via the Settings UI confirmation dialog.
|
||||
ShowBrightness = monitor.SupportsBrightness;
|
||||
ShowContrast = monitor.SupportsContrast;
|
||||
ShowVolume = monitor.SupportsVolume;
|
||||
ShowInputSource = monitor.SupportsInputSource;
|
||||
_showPowerState = monitor.SupportsPowerState;
|
||||
_showColorTemperature = monitor.SupportsColorTemperature;
|
||||
ShowInputSource = false;
|
||||
_showPowerState = false;
|
||||
_showColorTemperature = false;
|
||||
|
||||
// Initialize basic properties from monitor
|
||||
_brightness = monitor.CurrentBrightness;
|
||||
@@ -232,7 +238,9 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
|
||||
public string Id => _monitor.Id;
|
||||
|
||||
public string Name => _monitor.Name;
|
||||
public string Name => IsInternal
|
||||
? ResourceLoaderInstance.ResourceLoader.GetString("BuiltInDisplayName")
|
||||
: _monitor.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the monitor number from the underlying monitor model (Windows DISPLAY number)
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
ModuleImageSource="ms-appx:///Assets/Settings/Modules/GrabAndMove.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
|
||||
|
||||
<InfoBar
|
||||
x:Uid="GrabAndMove_TouchpadInfoBar"
|
||||
Margin="0,0,0,8"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Informational" />
|
||||
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="GrabAndMoveEnableToggleControlHeaderText"
|
||||
|
||||
@@ -211,7 +211,11 @@
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableVolume" IsChecked="{x:Bind EnableVolume, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsInputSource, Mode=OneWay}">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableInputSource" IsChecked="{x:Bind EnableInputSource, Mode=TwoWay}" />
|
||||
<CheckBox
|
||||
x:Uid="PowerDisplay_Monitor_EnableInputSource"
|
||||
Click="EnableInputSource_Click"
|
||||
IsChecked="{x:Bind EnableInputSource, Mode=TwoWay}"
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnableRotation" IsChecked="{x:Bind EnableRotation, Mode=TwoWay}" />
|
||||
@@ -224,7 +228,11 @@
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsPowerState, Mode=OneWay}">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_EnablePowerState" IsChecked="{x:Bind EnablePowerState, Mode=TwoWay}" />
|
||||
<CheckBox
|
||||
x:Uid="PowerDisplay_Monitor_EnablePowerState"
|
||||
Click="EnablePowerState_Click"
|
||||
IsChecked="{x:Bind EnablePowerState, Mode=TwoWay}"
|
||||
Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left">
|
||||
<CheckBox x:Uid="PowerDisplay_Monitor_HideMonitor" IsChecked="{x:Bind IsHidden, Mode=TwoWay}" />
|
||||
|
||||
@@ -209,13 +209,40 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to prevent reentrant handling during programmatic checkbox changes
|
||||
private bool _isRestoringColorTempCheckbox;
|
||||
// Flag to prevent reentrant Click handling while we programmatically restore
|
||||
// a checkbox after the user cancels a dangerous-feature confirmation dialog.
|
||||
private bool _isRestoringDangerousFeatureCheckbox;
|
||||
|
||||
private async void EnableColorTemperature_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Skip if we're programmatically restoring the checkbox state
|
||||
if (_isRestoringColorTempCheckbox)
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_ColorTemperature",
|
||||
(monitor, value) => monitor.EnableColorTemperature = value);
|
||||
}
|
||||
|
||||
private async void EnablePowerState_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_PowerState",
|
||||
(monitor, value) => monitor.EnablePowerState = value);
|
||||
}
|
||||
|
||||
private async void EnableInputSource_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await HandleDangerousFeatureClickAsync(
|
||||
sender,
|
||||
"PowerDisplay_InputSource",
|
||||
(monitor, value) => monitor.EnableInputSource = value);
|
||||
}
|
||||
|
||||
private async Task HandleDangerousFeatureClickAsync(
|
||||
object sender,
|
||||
string resourceKeyPrefix,
|
||||
Action<MonitorInfo, bool> setter)
|
||||
{
|
||||
if (_isRestoringDangerousFeatureCheckbox)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -225,18 +252,17 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show warning when enabling (checking the box)
|
||||
// Only show the warning when the user is enabling the feature.
|
||||
if (checkBox.IsChecked != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog with color temperature warning
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
XamlRoot = this.XamlRoot,
|
||||
Title = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningTitle"),
|
||||
Title = resourceLoader.GetString($"{resourceKeyPrefix}_WarningTitle"),
|
||||
Content = new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
@@ -244,31 +270,31 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningHeader"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningHeader"),
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.Bold,
|
||||
Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"],
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningDescription"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningDescription"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningList"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningList"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(20, 0, 0, 0),
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("PowerDisplay_ColorTemperature_WarningConfirm"),
|
||||
Text = resourceLoader.GetString($"{resourceKeyPrefix}_WarningConfirm"),
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
},
|
||||
},
|
||||
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_ColorTemperature_EnableButton"),
|
||||
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Enable"),
|
||||
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel"),
|
||||
DefaultButton = ContentDialogButton.Close,
|
||||
};
|
||||
@@ -277,16 +303,16 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
if (result != ContentDialogResult.Primary)
|
||||
{
|
||||
// User cancelled: revert checkbox to unchecked
|
||||
_isRestoringColorTempCheckbox = true;
|
||||
// User cancelled: revert checkbox to unchecked.
|
||||
_isRestoringDangerousFeatureCheckbox = true;
|
||||
try
|
||||
{
|
||||
checkBox.IsChecked = false;
|
||||
monitor.EnableColorTemperature = false;
|
||||
setter(monitor, false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRestoringColorTempCheckbox = false;
|
||||
_isRestoringDangerousFeatureCheckbox = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5501,7 +5501,41 @@ The break timer font matches the text font.</value>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningConfirm" xml:space="preserve">
|
||||
<value>Do you want to enable color temperature control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_EnableButton" xml:space="preserve">
|
||||
<data name="PowerDisplay_PowerState_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm power state control</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningHeader" xml:space="preserve">
|
||||
<value>⚠️ Warning: This action may be unsafe.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningDescription" xml:space="preserve">
|
||||
<value>Enabling power state control may lead to unexpected behavior, including:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningList" xml:space="preserve">
|
||||
<value>• Monitor may enter standby and not wake via software.
|
||||
• You may need to press the monitor’s power button or reconnect the cable to recover.
|
||||
• Some monitors may not restore the previous state correctly.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable power state control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm input source control</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningHeader" xml:space="preserve">
|
||||
<value>⚠️ Warning: This action may be unsafe.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningDescription" xml:space="preserve">
|
||||
<value>Switching input sources may cause unintended results, including:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningList" xml:space="preserve">
|
||||
<value>• Screen may go black if the selected input has no signal.
|
||||
• Software control may be lost until you switch back using the monitor’s physical buttons.
|
||||
• Some monitors may not reliably expose all input sources.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable input source control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Dialog_Enable" xml:space="preserve">
|
||||
<value>Enable</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Dialog_Cancel" xml:space="preserve">
|
||||
@@ -5943,6 +5977,13 @@ Text uses the current drawing color.</value>
|
||||
<value>Grab And Move</value>
|
||||
<comment>"Grab And Move" is the name of the utility</comment>
|
||||
</data>
|
||||
<data name="GrabAndMove_TouchpadInfoBar.Title" xml:space="preserve">
|
||||
<value>Touchpad support</value>
|
||||
</data>
|
||||
<data name="GrabAndMove_TouchpadInfoBar.Message" xml:space="preserve">
|
||||
<value>Grab And Move may not work reliably with some touchpads.</value>
|
||||
<comment>"Grab And Move" is the name of the utility</comment>
|
||||
</data>
|
||||
<data name="LearnMore_GrabAndMove.Text" xml:space="preserve">
|
||||
<value>Learn more about Grab And Move</value>
|
||||
<comment>"Grab And Move" is the name of the utility</comment>
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Timers;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -18,7 +19,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
|
||||
public partial class ImageResizerViewModel : Observable
|
||||
public partial class ImageResizerViewModel : Observable, IDisposable
|
||||
{
|
||||
private static readonly string DefaultPresetNamePrefix =
|
||||
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_DefaultSize_NewSizePrefix");
|
||||
@@ -43,6 +44,13 @@ public partial class ImageResizerViewModel : Observable
|
||||
/// </summary>
|
||||
private readonly ImageSize _customSize;
|
||||
|
||||
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval
|
||||
private const int SaveSettingsDelayInMs = 500;
|
||||
|
||||
private readonly System.Threading.Lock _delayedActionLock = new System.Threading.Lock();
|
||||
private Timer _delayedTimer;
|
||||
private bool _disposed;
|
||||
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
@@ -98,6 +106,11 @@ public partial class ImageResizerViewModel : Observable
|
||||
_customSize = Settings.Properties.ImageresizerCustomSize.Value;
|
||||
|
||||
_isInitializing = false;
|
||||
|
||||
_delayedTimer = new Timer();
|
||||
_delayedTimer.Interval = SaveSettingsDelayInMs;
|
||||
_delayedTimer.Elapsed += DelayedTimer_Tick;
|
||||
_delayedTimer.AutoReset = false;
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
@@ -337,9 +350,61 @@ public partial class ImageResizerViewModel : Observable
|
||||
|
||||
public void SizePropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
ScheduleSaveImageSizes();
|
||||
}
|
||||
|
||||
private void ScheduleSaveImageSizes()
|
||||
{
|
||||
lock (_delayedActionLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_delayedTimer.Enabled)
|
||||
{
|
||||
_delayedTimer.Stop();
|
||||
}
|
||||
|
||||
_delayedTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void DelayedTimer_Tick(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
lock (_delayedActionLock)
|
||||
{
|
||||
_delayedTimer.Stop();
|
||||
}
|
||||
|
||||
SaveImageSizes();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
lock (_delayedActionLock)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_delayedTimer?.Stop();
|
||||
_delayedTimer?.Dispose();
|
||||
_delayedTimer = null;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateNameForNewSize(string namePrefix)
|
||||
{
|
||||
int newSizeCounter = 0;
|
||||
|
||||
Reference in New Issue
Block a user